From 6849fe51d0aad074f8ad897e6b7909f84b411550 Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Fri, 11 Aug 2023 15:07:59 +0300 Subject: [PATCH] feat(jans-auth-server): OAuth 2.0 for First-Party Native Applications (#5654) * feat(jans-auth-server): added authorization challenge endpoint config and discovery #5563 * feat(jans-auth-server): renamed authorization_challenge_request_endpoint -> authorization_challenge_endpoint https://github.com/JanssenProject/jans/issues/5563 * feat(jans-auth-server): added device session with attributes #5563 * feat(jans-auth-server): added authorization challenge custom script #5563 * feat(jans-auth-server): implemented authorization challenge service #5563 * feat(jans-auth-server): added authorization challenge validator with test #5563 * feat(jans-auth-server): added device session service #5563 * feat(jans-auth-server): buildfix #5563 * feat(jans-auth-server): added session support to authorization challenge #5563 * feat(jans-auth-server): authorization challenge endpoint #5563 * feat(jans-auth-server): minor improvements in authorization endpoint #5563 * feat(jans-auth-server): improved external script context (need it for authorization challenge) #5563 * feat(jans-auth-server): added external service impl for authz challenge #5781 * fix(jans-auth-server): added validation test to testng * feat(jans-auth-server): added sample custom script for authz challenge #5781 * feat(jans-linux-setup): added default authz challenge custom script to setup #5781 * fix(jans-linux-setup): fixed path to authz challenge custom script #5781 * chore(jans-auth-server): removed redundant code #5563 * fix(jans-auth-server): fixed authz challenge endpoint initialization #5563 * fix(jans-auth-server): corrected challenge endpoint #5563 * fix(jans-auth-server): npe fix in challenge endpoint #5563 * test(jans-auth-server): added integration test for authorization challenge #5563 * doc(jans-auth-server): documented Authorization Challenge implementation #5563 Signed-off-by: Mustafa Baser --- .../endpoints/authorization-challenge.md | 636 ++++++++++++++++++ .../auth-server/endpoints/authorization.md | 4 - .../auth-server/oauth-features/README.md | 1 + .../jans-authorization-server-config.md | 1 + docs/admin/developer/interception-scripts.md | 1 + .../scripts/authorization-challenge.md | 246 +++++++ .../AuthorizationChallenge.java | 177 +++++ .../jans/as/client/AuthorizationResponse.java | 14 +- .../as/client/OpenIdConfigurationClient.java | 1 + .../client/OpenIdConfigurationResponse.java | 20 + .../test/java/io/jans/as/client/BaseTest.java | 11 + .../as/client/ciba/ConfigurationTest.java | 1 + .../io/jans/as/client/client/Asserter.java | 1 + .../ws/rs/AuthorizationChallengeHttpTest.java | 188 ++++++ .../client/src/test/resources/testng.xml | 6 +- .../common/model/session/DeviceSession.java | 110 +++ .../session/DeviceSessionAttributes.java | 48 ++ jans-auth-server/docs/swagger.yaml | 78 +++ .../AuthorizationChallengeResponse.java | 44 ++ .../io/jans/as/model/config/Constants.java | 1 + .../model/configuration/AppConfiguration.java | 67 ++ .../ConfigurationResponseClaim.java | 1 + .../as/model/discovery/OAuth2Discovery.java | 13 + .../as/model/error/ErrorResponseFactory.java | 16 +- jans-auth-server/server/conf/jans-config.json | 1 + .../ws/rs/AuthorizationChallengeEndpoint.java | 63 ++ .../ws/rs/AuthorizationChallengeService.java | 208 ++++++ .../rs/AuthorizationChallengeValidator.java | 63 ++ .../ws/rs/AuthorizeRestWebServiceImpl.java | 17 +- .../rs/AuthorizeRestWebServiceValidator.java | 4 + .../server/authorize/ws/rs/AuthzRequest.java | 30 + .../authorize/ws/rs/AuthzRequestService.java | 14 +- .../authorize/ws/rs/DeviceSessionService.java | 62 ++ .../io/jans/as/server/model/audit/Action.java | 1 + .../server/service/ResteasyInitializer.java | 12 +- ...ExternalAuthorizationChallengeService.java | 95 +++ .../context/DynamicScopeExternalContext.java | 3 +- .../context/ExternalScriptContext.java | 30 +- .../servlet/FapiOpenIdConfiguration.java | 1 + .../server/servlet/OpenIdConfiguration.java | 3 + .../as/server/uma/ws/rs/UmaMetadataWS.java | 1 + .../AuthorizationChallengeValidatorTest.java | 81 +++ .../service/TestResteasyInitializer.java | 16 +- .../server/src/test/resources/testng.xml | 1 + .../properties/endpoints/endpoints.json | 1 + .../model/custom/script/CustomScriptType.java | 3 + .../AuthorizationChallengeType.java | 11 + .../DummyAuthorizationChallengeType.java | 37 + .../jans_setup/schema/jans_schema.json | 23 + .../templates/jans-auth/jans-auth-config.json | 1 + .../jans_setup/templates/scripts.ldif | 14 + 51 files changed, 2433 insertions(+), 49 deletions(-) create mode 100644 docs/admin/auth-server/endpoints/authorization-challenge.md create mode 100644 docs/admin/developer/scripts/authorization-challenge.md create mode 100644 docs/script-catalog/authorization_challenge/AuthorizationChallenge.java create mode 100644 jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/AuthorizationChallengeHttpTest.java create mode 100644 jans-auth-server/common/src/main/java/io/jans/as/common/model/session/DeviceSession.java create mode 100644 jans-auth-server/common/src/main/java/io/jans/as/common/model/session/DeviceSessionAttributes.java create mode 100644 jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizationChallengeResponse.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeEndpoint.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeValidator.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/DeviceSessionService.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthorizationChallengeService.java create mode 100644 jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeValidatorTest.java create mode 100644 jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/AuthorizationChallengeType.java create mode 100644 jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/DummyAuthorizationChallengeType.java diff --git a/docs/admin/auth-server/endpoints/authorization-challenge.md b/docs/admin/auth-server/endpoints/authorization-challenge.md new file mode 100644 index 00000000000..ae587b6b9f2 --- /dev/null +++ b/docs/admin/auth-server/endpoints/authorization-challenge.md @@ -0,0 +1,636 @@ +--- +tags: +- administration +- auth-server +- authorization-challenge +- endpoint +--- + +# Overview + +Authorization Challenge Endpoint allows first-party native client obtain authorization code which later can be exchanged on access token. +This can provide an entirely browserless OAuth 2.0 experience suited for native applications. + +This endpoint conforms to [OAuth 2.0 for First-Party Native Applications](https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-native-apps-00.html) specifications. + +URL to access authorization challenge endpoint on Janssen Server is listed in the response of Janssen Server's well-known +[configuration endpoint](./configuration.md) given below. + +```text +https://janssen.server.host/jans-auth/.well-known/openid-configuration +``` + +`authorization_challenge_endpoint` claim in the response specifies the URL for authorization challenge endpoint. By default, authorization +challenge endpoint looks like below: + +``` +https://janssen.server.host/jans-auth/restv1/authorization_challenge +``` + +More information about request and response of the authorization challenge endpoint can be found in the OpenAPI specification +of [jans-auth-server module](https://gluu.org/swagger-ui/?url=https://raw.githubusercontent.com/JanssenProject/jans/vreplace-janssen-version/jans-auth-server/docs/swagger.yaml#/Authorization_Challenge). + +Sample request +``` +POST /authorize HTTP/1.1 +Host: server.example.com +Content-Type: application/x-www-form-urlencoded + +login_hint=%2B1-310-123-4567&scope=profile +&client_id=bb16c14c73415 +``` + +Sample successful response with `authorization_code`. +``` +HTTP/1.1 200 OK +Content-Type: application/json;charset=UTF-8 +Cache-Control: no-store + +{ + "authorization_code": "uY29tL2F1dGhlbnRpY" +} +``` + +Sample error response +``` +HTTP/1.1 401 Unauthorized +Content-Type: application/json +Cache-Control: no-store + +{ + "error": "username_required" +} +``` + + +## Configuration Properties + +Authorization Challenge Endpoint AS configuration: + +- **authorizationChallengeEndpoint** - The authorization challenge endpoint URL +- **authorizationChallengeDefaultAcr** - Authorization Challenge Endpoint Default ACR if no value is specified in acr_values request parameter. Default value is `default_challenge`. +- **authorizationChallengeShouldGenerateSession** - Boolean value specifying whether to generate session_id (AS object and cookie) during authorization at Authorization Challenge Endpoint. Default value is `false`. +- **mtlsAuthorizationChallengeEndpoint** - URL for Mutual TLS (mTLS) Client Authentication and Certificate-Bound Access Tokens (MTLS) Authorization Challenge Endpoint. + +## Custom script + +AS provides `AuthorizationChallengeType` custom script which must be used to control Authorization Challenge Endpoint behaviour. + +If request does not have `acr_values` specified and script name falls back to `default_challenge` which is available and enabled during installation. +Default script name can be changed via `authorizationChallengeDefaultAcr` configuration property. + +Main method return true/false which indicates to server whether to issue `authorization_code` in response or not. + +If parameters is not present then error has to be created and `false` returned. +If all is good script has to return `true` and it's strongly recommended to set user `context.getExecutionContext().setUser(user);` so AS can keep tracking what exactly user is authenticated. + +Please see following snippet below: + +```java + public boolean authorize(Object scriptContext) { + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + + // 1. validate all required parameters are present + final String username = getParameterOrCreateError(context, USERNAME_PARAMETER); + if (StringUtils.isBlank(username)) { + return false; + } + + final String password = getParameterOrCreateError(context, PASSWORD_PARAMETER); + if (StringUtils.isBlank(password)) { + return false; + } + + scriptLogger.trace("All required parameters are present"); + + // 2. main authorization logic, if ok -> set authorized user into "context.getExecutionContext().setUser(user);" and return true + UserService userService = CdiUtil.bean(UserService.class); + PersistenceEntryManager entryManager = CdiUtil.bean(PersistenceEntryManager.class); + + final User user = userService.getUser(username); + if (user == null) { + scriptLogger.trace("User is not found by username {}", username); + createError(context, "username_invalid"); + return false; + } + + final boolean ok = entryManager.authenticate(user.getDn(), User.class, password); + if (ok) { + context.getExecutionContext().setUser(user); // <- IMPORTANT : without user set, user relation will not be associated with token + scriptLogger.trace("User {} is authenticated successfully.", username); + return true; + } + + // 3. not ok -> set error which explains what is wrong and return false + scriptLogger.trace("Failed to authenticate user {}. Please check username and password.", username); + createError(context, "username_or_password_invalid"); + return false; + } +``` + +More details in [Authorization Challenge Custom Script Page](../../developer/scripts/authorization-challenge.md). + +Full sample script can be found [here](../../../script-catalog/authorization_challenge/AuthorizationChallenge.java) + +## Device session + +Device session is optional. AS does not return it by default. +It's possible to pass in request `use_device_session=true` which makes AS return it in error response. + +## Full successful Authorization Challenge Flow sample + +``` +OpenID Connect Configuration +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +GET /.well-known/openid-configuration HTTP/1.1 HTTP/1.1 +Host: yuriyz-fond-skink.gluu.info + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Connection: Keep-Alive +Content-Length: 6244 +Content-Type: application/json +Date: Thu, 10 Aug 2023 11:53:04 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=fa097a44-5568-48aa-9390-1880616e5a69; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{ + "request_parameter_supported" : true, + "pushed_authorization_request_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/par", + "introspection_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/introspection", + "claims_parameter_supported" : false, + "issuer" : "https://yuriyz-fond-skink.gluu.info", + "userinfo_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "id_token_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "access_token_signing_alg_values_supported" : [ "none", "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "authorization_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/authorize", + "service_documentation" : "http://jans.org/docs", + "authorization_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "claims_supported" : [ "street_address", "country", "zoneinfo", "birthdate", "role", "gender", "formatted", "user_name", "phone_mobile_number", "preferred_username", "locale", "inum", "updated_at", "post_office_box", "nickname", "preferred_language", "email", "website", "email_verified", "profile", "locality", "phone_number_verified", "room_number", "given_name", "middle_name", "picture", "name", "phone_number", "postal_code", "region", "family_name", "jansAdminUIRole" ], + "ssa_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/ssa", + "token_endpoint_auth_methods_supported" : [ "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "tls_client_auth", "self_signed_tls_client_auth" ], + "tls_client_certificate_bound_access_tokens" : true, + "response_modes_supported" : [ "query", "jwt", "query.jwt", "form_post.jwt", "form_post", "fragment", "fragment.jwt" ], + "backchannel_logout_session_supported" : true, + "token_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/token", + "response_types_supported" : [ "code", "code id_token token", "id_token", "code id_token", "token", "id_token token", "code token" ], + "authorization_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "backchannel_token_delivery_modes_supported" : [ "poll", "ping", "push" ], + "dpop_signing_alg_values_supported" : [ "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "request_uri_parameter_supported" : true, + "backchannel_user_code_parameter_supported" : false, + "grant_types_supported" : [ "client_credentials", "urn:ietf:params:oauth:grant-type:uma-ticket", "urn:ietf:params:oauth:grant-type:device_code", "urn:ietf:params:oauth:grant-type:token-exchange", "implicit", "authorization_code", "password", "refresh_token" ], + "ui_locales_supported" : [ "en", "bg", "de", "es", "fr", "it", "ru", "tr" ], + "userinfo_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/userinfo", + "authorization_challenge_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/authorization_challenge", + "op_tos_uri" : "https://yuriyz-fond-skink.gluu.info/tos", + "require_request_uri_registration" : false, + "id_token_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "frontchannel_logout_session_supported" : true, + "authorization_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "claims_locales_supported" : [ "en" ], + "clientinfo_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/clientinfo", + "request_object_signing_alg_values_supported" : [ "none", "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "request_object_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "key_from_java" : "value_from_script_on_java", + "session_revocation_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/revoke_session", + "check_session_iframe" : "https://yuriyz-fond-skink.gluu.info/jans-auth/opiframe.htm", + "scopes_supported" : [ "address", "introspection", "https://jans.io/auth/ssa.admin", "online_access", "openid", "user_name", "clientinfo", "profile", "uma_protection", "permission", "https://jans.io/scim/users.write", "revoke_session", "https://jans.io/scim/users.read", "device_sso", "phone", "mobile_phone", "offline_access", "email" ], + "backchannel_logout_supported" : true, + "acr_values_supported" : [ "simple_password_auth" ], + "request_object_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "device_authorization_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/device_authorization", + "display_values_supported" : [ "page", "popup" ], + "userinfo_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "require_pushed_authorization_requests" : false, + "claim_types_supported" : [ "normal" ], + "userinfo_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "end_session_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/end_session", + "revocation_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/revoke", + "backchannel_authentication_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/bc-authorize", + "token_endpoint_auth_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "frontchannel_logout_supported" : true, + "jwks_uri" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/jwks", + "subject_types_supported" : [ "public", "pairwise" ], + "id_token_signing_alg_values_supported" : [ "none", "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "registration_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/register", + "id_token_token_binding_cnf_values_supported" : [ "tbh" ] +} + + +####################################################### +TEST: authorizationChallengeFlow +####################################################### +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +POST /jans-auth/restv1/register HTTP/1.1 +Host: yuriyz-fond-skink.gluu.info +Content-Type: application/json +Accept: application/json + +{ + "grant_types" : [ "authorization_code", "refresh_token" ], + "subject_type" : "public", + "application_type" : "web", + "scope" : "openid profile address email phone user_name", + "minimum_acr_priority_list" : [ ], + "redirect_uris" : [ "https://yuriyz-fond-skink.gluu.info/jans-auth-rp/home.htm", "https://client.example.com/cb", "https://client.example.com/cb1", "https://client.example.com/cb2" ], + "client_name" : "jans test app", + "additional_audience" : [ ], + "response_types" : [ "code", "id_token" ] +} + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 201 +Cache-Control: no-store +Connection: Keep-Alive +Content-Length: 1633 +Content-Type: application/json +Date: Thu, 10 Aug 2023 11:53:05 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Pragma: no-cache +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=81dc6c45-7831-4738-b169-b087ee9a6bd6; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{ + "allow_spontaneous_scopes": false, + "application_type": "web", + "rpt_as_jwt": false, + "registration_client_uri": "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/register?client_id=999e13b8-f4a2-4fed-ad3c-6c88bd2c92ea", + "tls_client_auth_subject_dn": "", + "run_introspection_script_before_jwt_creation": false, + "registration_access_token": "28a50db3-b6d1-4054-a259-ef7168afa760", + "client_id": "999e13b8-f4a2-4fed-ad3c-6c88bd2c92ea", + "token_endpoint_auth_method": "client_secret_basic", + "scope": "openid", + "client_secret": "f6364c5c-295d-4e6e-bb40-6ad3a47b2119", + "client_id_issued_at": 1691668385, + "backchannel_logout_uri": [], + "backchannel_logout_session_required": false, + "client_name": "jans test app", + "par_lifetime": 600, + "spontaneous_scopes": [], + "id_token_signed_response_alg": "RS256", + "access_token_as_jwt": false, + "grant_types": [ + "authorization_code", + "refresh_token" + ], + "subject_type": "public", + "additional_token_endpoint_auth_methods": [], + "keep_client_authorization_after_expiration": false, + "require_par": false, + "redirect_uris": [ + "https://client.example.com/cb2", + "https://client.example.com/cb1", + "https://client.example.com/cb", + "https://yuriyz-fond-skink.gluu.info/jans-auth-rp/home.htm" + ], + "redirect_uris_regex": "", + "additional_audience": [], + "frontchannel_logout_session_required": false, + "client_secret_expires_at": 1691704385, + "access_token_signing_alg": "RS256", + "response_types": [ + "code", + "id_token" + ] +} + +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +POST /jans-auth/restv1/authorization_challenge HTTP/1.1 +Host: yuriyz-fond-skink.gluu.info + +client_id=999e13b8-f4a2-4fed-ad3c-6c88bd2c92ea&scope=openid+profile+address+email+phone+user_name&state=b4a41b29-51c8-4354-9c8c-fda38b4dbd43&nonce=3a56f8d0-f78e-4b15-857c-3e792801be68&prompt=&ui_locales=&claims_locales=&acr_values=&request_session_id=false&password=secret&username=admin + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Cache-Control: no-transform, no-store +Connection: Keep-Alive +Content-Length: 61 +Content-Type: application/json +Date: Thu, 10 Aug 2023 11:53:06 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=3aa95eb7-73e2-40ae-9303-34adf30a1a05; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{"authorization_code":"9e3dc65b-937a-49c2-bdff-41fbc1a352d0"} + +Successfully obtained authorization code 9e3dc65b-937a-49c2-bdff-41fbc1a352d0 at Authorization Challenge Endpoint +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +POST /jans-auth/restv1/token HTTP/1.1 +Host: yuriyz-fond-skink.gluu.info +Content-Type: application/x-www-form-urlencoded +Authorization: Basic OTk5ZTEzYjgtZjRhMi00ZmVkLWFkM2MtNmM4OGJkMmM5MmVhOmY2MzY0YzVjLTI5NWQtNGU2ZS1iYjQwLTZhZDNhNDdiMjExOQ== + +grant_type=authorization_code&code=9e3dc65b-937a-49c2-bdff-41fbc1a352d0&redirect_uri=https%3A%2F%2Fyuriyz-fond-skink.gluu.info%2Fjans-auth-rp%2Fhome.htm + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Cache-Control: no-store +Connection: Keep-Alive +Content-Length: 1250 +Content-Type: application/json +Date: Thu, 10 Aug 2023 11:53:06 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Pragma: no-cache +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=3eb3c205-6206-4a70-98fb-75bf81757976; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{"access_token":"d87aa8d2-fefb-4d16-a775-9b9d27f73bfc","refresh_token":"505314a7-05f5-4c9f-8900-ccb0685dea17","id_token":"eyJraWQiOiJjb25uZWN0XzI1OGZmMmFiLWE4ODQtNDIxNy1iNmQ4LTJhMGI2NDhmOTcxZF9zaWdfcnMyNTYiLCJ0eXAiOiJqd3QiLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiUC04RktlejhlNHROTURTbVlGeHV5dyIsInN1YiI6IjI1Nzg0ZDQ5LTg0ZjMtNGIyNi1hZWUyLTEwNDkzMzM5MjMyZCIsImFtciI6W10sImlzcyI6Imh0dHBzOi8veXVyaXl6LWZvbmQtc2tpbmsuZ2x1dS5pbmZvIiwibm9uY2UiOiIzYTU2ZjhkMC1mNzhlLTRiMTUtODU3Yy0zZTc5MjgwMWJlNjgiLCJqYW5zT3BlbklEQ29ubmVjdFZlcnNpb24iOiJvcGVuaWRjb25uZWN0LTEuMCIsImF1ZCI6Ijk5OWUxM2I4LWY0YTItNGZlZC1hZDNjLTZjODhiZDJjOTJlYSIsInJhbmRvbSI6ImJmYmI5OTBmLWNkYTEtNGM3OC1hNjM4LWFhN2NiMjc5MjU3MiIsImFjciI6ImRlZmF1bHRfY2hhbGxlbmdlIiwiY19oYXNoIjoiTnFoSGFIenZZYjYxeDFackQwUEZVdyIsImF1dGhfdGltZSI6MTY5MTY2ODM4NiwiZXhwIjoxNjkxNjcxOTg2LCJncmFudCI6ImF1dGhvcml6YXRpb25fY29kZSIsImlhdCI6MTY5MTY2ODM4Nn0.QTUmzJaHtbPGjrV4E0MUn_fU1On44B6-_7pT0Dz_cY29s_KajGLfin3G_WsYmZA--ysyRLAmdK_X5C3W-wpkpDJ8906vuZST5547lSJGOZ45_VFv7XnTmBip3zRQOmrlxdU6OQ5Vmj3xMON_NQ-ckEUSNr65xWTAPmOQoncGYp8s-TO7ethyx6UyDTnW8d1YiXWCUYfQDQ8d5wCPHnfoYAsZCs_f0xaBUmaiwvUL3ckiXgMr2yHjWKWQuezlbjJk7ODu2cgoAzs3IWMonaixIJeeJJcOvFB4SPTnbToJe7ISvvsZTEwrLWW_E_LgTUEDqHbeWyeQI8WqDa9EOwMEFw","token_type":"Bearer","expires_in":299} + +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +POST /jans-auth/restv1/token HTTP/1.1 +Host: yuriyz-fond-skink.gluu.info +Content-Type: application/x-www-form-urlencoded +Authorization: Basic OTk5ZTEzYjgtZjRhMi00ZmVkLWFkM2MtNmM4OGJkMmM5MmVhOmY2MzY0YzVjLTI5NWQtNGU2ZS1iYjQwLTZhZDNhNDdiMjExOQ== + +grant_type=refresh_token&refresh_token=505314a7-05f5-4c9f-8900-ccb0685dea17 + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Cache-Control: no-store +Connection: Keep-Alive +Content-Length: 166 +Content-Type: application/json +Date: Thu, 10 Aug 2023 11:53:08 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Pragma: no-cache +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=88a6b7a5-3230-4f0f-b859-09df77a5c67a; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{"access_token":"572f6422-caf9-496a-a6be-4ab39c872816","refresh_token":"e93b6576-5297-4d9d-a92b-3276d90a75e4","scope":"openid","token_type":"Bearer","expires_in":299} + +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +GET /jans-auth/restv1/userinfo HTTP/1.1 HTTP/1.1 +Host: yuriyz-fond-skink.gluu.info +Authorization: Bearer 572f6422-caf9-496a-a6be-4ab39c872816 + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Cache-Control: no-store, private +Connection: Keep-Alive +Content-Length: 46 +Content-Type: application/json;charset=utf-8 +Date: Thu, 10 Aug 2023 11:53:08 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Pragma: no-cache +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=390c7a63-fe06-48a5-b3bf-2549267ba9b0; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{"sub":"25784d49-84f3-4b26-aee2-10493339232d"} + +``` + +## Authorization Challenge Flow sample with invalid user + +``` +OpenID Connect Configuration +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +GET /.well-known/openid-configuration HTTP/1.1 HTTP/1.1 +Host: yuriyz-fond-skink.gluu.info + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Connection: Keep-Alive +Content-Length: 6244 +Content-Type: application/json +Date: Thu, 10 Aug 2023 11:57:01 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=79c5fed3-d69a-4fdf-af88-fce550cd1819; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{ + "request_parameter_supported" : true, + "pushed_authorization_request_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/par", + "introspection_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/introspection", + "claims_parameter_supported" : false, + "issuer" : "https://yuriyz-fond-skink.gluu.info", + "userinfo_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "id_token_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "access_token_signing_alg_values_supported" : [ "none", "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "authorization_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/authorize", + "service_documentation" : "http://jans.org/docs", + "authorization_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "claims_supported" : [ "street_address", "country", "zoneinfo", "birthdate", "role", "gender", "formatted", "user_name", "phone_mobile_number", "preferred_username", "locale", "inum", "updated_at", "post_office_box", "nickname", "preferred_language", "email", "website", "email_verified", "profile", "locality", "phone_number_verified", "room_number", "given_name", "middle_name", "picture", "name", "phone_number", "postal_code", "region", "family_name", "jansAdminUIRole" ], + "ssa_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/ssa", + "token_endpoint_auth_methods_supported" : [ "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "tls_client_auth", "self_signed_tls_client_auth" ], + "tls_client_certificate_bound_access_tokens" : true, + "response_modes_supported" : [ "query", "jwt", "query.jwt", "form_post.jwt", "form_post", "fragment", "fragment.jwt" ], + "backchannel_logout_session_supported" : true, + "token_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/token", + "response_types_supported" : [ "code", "code id_token token", "id_token", "code id_token", "token", "id_token token", "code token" ], + "authorization_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "backchannel_token_delivery_modes_supported" : [ "poll", "ping", "push" ], + "dpop_signing_alg_values_supported" : [ "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "request_uri_parameter_supported" : true, + "backchannel_user_code_parameter_supported" : false, + "grant_types_supported" : [ "client_credentials", "urn:ietf:params:oauth:grant-type:uma-ticket", "urn:ietf:params:oauth:grant-type:device_code", "urn:ietf:params:oauth:grant-type:token-exchange", "implicit", "authorization_code", "password", "refresh_token" ], + "ui_locales_supported" : [ "en", "bg", "de", "es", "fr", "it", "ru", "tr" ], + "userinfo_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/userinfo", + "authorization_challenge_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/authorization_challenge", + "op_tos_uri" : "https://yuriyz-fond-skink.gluu.info/tos", + "require_request_uri_registration" : false, + "id_token_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "frontchannel_logout_session_supported" : true, + "authorization_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "claims_locales_supported" : [ "en" ], + "clientinfo_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/clientinfo", + "request_object_signing_alg_values_supported" : [ "none", "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "request_object_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "key_from_java" : "value_from_script_on_java", + "session_revocation_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/revoke_session", + "check_session_iframe" : "https://yuriyz-fond-skink.gluu.info/jans-auth/opiframe.htm", + "scopes_supported" : [ "address", "introspection", "https://jans.io/auth/ssa.admin", "online_access", "openid", "user_name", "clientinfo", "profile", "uma_protection", "permission", "https://jans.io/scim/users.write", "revoke_session", "https://jans.io/scim/users.read", "device_sso", "phone", "mobile_phone", "offline_access", "email" ], + "backchannel_logout_supported" : true, + "acr_values_supported" : [ "simple_password_auth" ], + "request_object_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "device_authorization_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/device_authorization", + "display_values_supported" : [ "page", "popup" ], + "userinfo_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "require_pushed_authorization_requests" : false, + "claim_types_supported" : [ "normal" ], + "userinfo_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "end_session_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/end_session", + "revocation_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/revoke", + "backchannel_authentication_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/bc-authorize", + "token_endpoint_auth_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "frontchannel_logout_supported" : true, + "jwks_uri" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/jwks", + "subject_types_supported" : [ "public", "pairwise" ], + "id_token_signing_alg_values_supported" : [ "none", "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "registration_endpoint" : "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/register", + "id_token_token_binding_cnf_values_supported" : [ "tbh" ] +} + + +####################################################### +TEST: authorizationChallengeFlow +####################################################### +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +POST /jans-auth/restv1/register HTTP/1.1 +Host: yuriyz-fond-skink.gluu.info +Content-Type: application/json +Accept: application/json + +{ + "grant_types" : [ "authorization_code", "refresh_token" ], + "subject_type" : "public", + "application_type" : "web", + "scope" : "openid profile address email phone user_name", + "minimum_acr_priority_list" : [ ], + "redirect_uris" : [ "https://yuriyz-fond-skink.gluu.info/jans-auth-rp/home.htm", "https://client.example.com/cb", "https://client.example.com/cb1", "https://client.example.com/cb2" ], + "client_name" : "jans test app", + "additional_audience" : [ ], + "response_types" : [ "code", "id_token" ] +} + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 201 +Cache-Control: no-store +Connection: Keep-Alive +Content-Length: 1633 +Content-Type: application/json +Date: Thu, 10 Aug 2023 11:57:02 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Pragma: no-cache +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=7045173c-9a96-418a-86ed-47a09749b004; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{ + "allow_spontaneous_scopes": false, + "application_type": "web", + "rpt_as_jwt": false, + "registration_client_uri": "https://yuriyz-fond-skink.gluu.info/jans-auth/restv1/register?client_id=d93a5129-1546-4b9b-bf8c-ea19e36ea2c8", + "tls_client_auth_subject_dn": "", + "run_introspection_script_before_jwt_creation": false, + "registration_access_token": "67aa99de-e977-4562-955d-6292f2c95df4", + "client_id": "d93a5129-1546-4b9b-bf8c-ea19e36ea2c8", + "token_endpoint_auth_method": "client_secret_basic", + "scope": "openid", + "client_secret": "f921c89c-57f0-4a91-baaa-036a4a22737b", + "client_id_issued_at": 1691668622, + "backchannel_logout_uri": [], + "backchannel_logout_session_required": false, + "client_name": "jans test app", + "par_lifetime": 600, + "spontaneous_scopes": [], + "id_token_signed_response_alg": "RS256", + "access_token_as_jwt": false, + "grant_types": [ + "authorization_code", + "refresh_token" + ], + "subject_type": "public", + "additional_token_endpoint_auth_methods": [], + "keep_client_authorization_after_expiration": false, + "require_par": false, + "redirect_uris": [ + "https://client.example.com/cb2", + "https://client.example.com/cb1", + "https://client.example.com/cb", + "https://yuriyz-fond-skink.gluu.info/jans-auth-rp/home.htm" + ], + "redirect_uris_regex": "", + "additional_audience": [], + "frontchannel_logout_session_required": false, + "client_secret_expires_at": 1691704622, + "access_token_signing_alg": "RS256", + "response_types": [ + "code", + "id_token" + ] +} + +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +POST /jans-auth/restv1/authorization_challenge HTTP/1.1 +Host: yuriyz-fond-skink.gluu.info + +client_id=d93a5129-1546-4b9b-bf8c-ea19e36ea2c8&scope=openid+profile+address+email+phone+user_name&state=4f925a8d-287a-4cba-a174-04d2e56109df&nonce=84c9b6dd-635c-4ca4-bba1-35c53c51a339&prompt=&ui_locales=&claims_locales=&acr_values=&request_session_id=false&password=secret&username=invalidUser + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 401 +Cache-Control: no-transform, no-store +Connection: Keep-Alive +Content-Length: 29 +Content-Type: application/json +Date: Thu, 10 Aug 2023 11:57:02 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=4c89e007-6c77-43da-a67f-b7ee1ff0e60a; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{"error": "username_invalid"} + +``` \ No newline at end of file diff --git a/docs/admin/auth-server/endpoints/authorization.md b/docs/admin/auth-server/endpoints/authorization.md index c9219e4ca97..e441070d599 100644 --- a/docs/admin/auth-server/endpoints/authorization.md +++ b/docs/admin/auth-server/endpoints/authorization.md @@ -30,10 +30,6 @@ https://janssen.server.host/jans-auth/restv1/authorize More information about request and response of the authorization endpoint can be found in the OpenAPI specification of [jans-auth-server module](https://gluu.org/swagger-ui/?url=https://raw.githubusercontent.com/JanssenProject/jans/vreplace-janssen-version/jans-auth-server/docs/swagger.yaml#/Authorization). -## Disabling The Endpoint Using Feature Flag - -TODO: It seems this endpoint can't be disabled using featureflags. Confirm this. - ## Configuration Properties diff --git a/docs/admin/auth-server/oauth-features/README.md b/docs/admin/auth-server/oauth-features/README.md index e8423f208f5..9ae240a4064 100644 --- a/docs/admin/auth-server/oauth-features/README.md +++ b/docs/admin/auth-server/oauth-features/README.md @@ -26,6 +26,7 @@ The [Janssen Authentication Server](https://github.com/JanssenProject/jans/tree/ - OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens [(spec)](https://datatracker.ietf.org/doc/html/rfc8705) - Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants [(spec)](https://www.rfc-editor.org/rfc/rfc7521.html) - JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) [(spec)](https://openid.net/specs/oauth-v2-jarm.html) +- OAuth 2.0 for First-Party Native Applications [(spec)](https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-native-apps-00.html) ## Protocol Overview diff --git a/docs/admin/config-guide/jans-authorization-server-config.md b/docs/admin/config-guide/jans-authorization-server-config.md index bd78e03f107..18758ac66c0 100644 --- a/docs/admin/config-guide/jans-authorization-server-config.md +++ b/docs/admin/config-guide/jans-authorization-server-config.md @@ -52,6 +52,7 @@ It returns all the information of the Jans Authorization server. "issuer": "https://example.jans.io", "baseEndpoint": "https://example.jans.io/jans-auth/restv1", "authorizationEndpoint": "https://example.jans.io/jans-auth/restv1/authorize", + "authorizationChallengeEndpoint":"https://example.jans.io/jans-auth/restv1/authorization_challenge", "tokenEndpoint": "https://example.jans.io/jans-auth/restv1/token", "tokenRevocationEndpoint": "https://example.jans.io/jans-auth/restv1/revoke", "userInfoEndpoint": "https://example.jans.io/jans-auth/restv1/userinfo", diff --git a/docs/admin/developer/interception-scripts.md b/docs/admin/developer/interception-scripts.md index ccde38487fe..039f80dd1df 100644 --- a/docs/admin/developer/interception-scripts.md +++ b/docs/admin/developer/interception-scripts.md @@ -36,6 +36,7 @@ calling external APIs 1. SCIM 1. [Introspection](./scripts/introspection.md) : Introspection scripts allows to modify response of Introspection Endpoint spec and present additional meta information surrounding the token. 1. [Post Authentication](./scripts/post-authentication.md) +1. [Authorization Challenge](./scripts/authorization-challenge.md) 1. [Select Account](./scripts/select-account.md) 1. Resource Owner Password Credentials 1. UMA 2 RPT Authorization Policies diff --git a/docs/admin/developer/scripts/authorization-challenge.md b/docs/admin/developer/scripts/authorization-challenge.md new file mode 100644 index 00000000000..ec15af72051 --- /dev/null +++ b/docs/admin/developer/scripts/authorization-challenge.md @@ -0,0 +1,246 @@ +--- +tags: + - administration + - developer + - scripts +--- + +# Authorization Challenge Custom Script + +## Overview + +The Jans-Auth server implements [OAuth 2.0 for First-Party Native Applications](https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-native-apps-00.html). +This script is used to control/customize Authorization Challenge Endpoint. + +## Behavior + +In request to Authorization Challenge Endpoint to is expected to have `acr_values` request parameter which specifies name of the custom script. +If parameter is absent or AS can't find script with this name then it falls back to script with name `default_challenge`. + +This script is provided during installation and performs basic `username`/`password` authentication. + +``` +POST /jans-auth/restv1/authorization_challenge HTTP/1.1 +Host: yuriyz-fond-skink.gluu.info + +client_id=999e13b8-f4a2-4fed-ad3c-6c88bd2c92ea&scope=openid+profile+address+email+phone+user_name&state=b4a41b29-51c8-4354-9c8c-fda38b4dbd43&nonce=3a56f8d0-f78e-4b15-857c-3e792801be68&acr_values=&request_session_id=false&password=secret&username=admin +``` + +There is **authorizationChallengeDefaultAcr** AS configuration property which allows to change fallback script name from `default_challenge` to some other value (value must be valid script name present on AS). + + +## Interface +The Authorization Challenage script implements the [AuthorizationChallenageType](https://github.com/JanssenProject/jans/blob/main/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/AuthorizationChallengeType.java) interface. This extends methods from the base script type in addition to adding new methods: + +### Inherited Methods +| Method header | Method description | +|:-----|:------| +| `def init(self, customScript, configurationAttributes)` | This method is only called once during the script initialization. It can be used for global script initialization, initiate objects etc | +| `def destroy(self, configurationAttributes)` | This method is called once to destroy events. It can be used to free resource and objects created in the `init()` method | +| `def getApiVersion(self, configurationAttributes, customScript)` | The getApiVersion method allows API changes in order to do transparent migration from an old script to a new API. Only include the customScript variable if the value for getApiVersion is greater than 10 | + +### New methods +| Method header | Method description | +|:-----|:------| +|`def authorize(self, context)`| Called when the request is received. | + +`authorize` method returns true/false which indicates to server whether to issue `authorization_code` in response or not. + +If parameters is not present then error has to be created and `false` returned. +If all is good script has to return `true` and it's strongly recommended to set user `context.getExecutionContext().setUser(user);` so AS can keep tracking what exactly user is authenticated. + + +### Objects +| Object name | Object description | +|:-----|:------| +|`customScript`| The custom script object. [Reference](https://github.com/JanssenProject/jans/blob/main/jans-core/script/src/main/java/io/jans/model/custom/script/model/CustomScript.java) | +|`context`| [Reference](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java) | + + +## Common Use Case: Authorize user by username/password + +### Script Type: Java + +```java +import io.jans.as.common.model.common.User; +import io.jans.as.common.model.session.DeviceSession; +import io.jans.as.server.authorize.ws.rs.DeviceSessionService; +import io.jans.as.server.service.UserService; +import io.jans.as.server.service.external.context.ExternalScriptContext; +import io.jans.model.SimpleCustomProperty; +import io.jans.model.custom.script.model.CustomScript; +import io.jans.model.custom.script.type.authzchallenge.AuthorizationChallengeType; +import io.jans.orm.PersistenceEntryManager; +import io.jans.service.cdi.util.CdiUtil; +import io.jans.service.custom.script.CustomScriptManager; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.UUID; + +/** + * @author Yuriy Z + */ +public class AuthorizationChallenge implements AuthorizationChallengeType { + + public static final String USERNAME_PARAMETER = "username"; + public static final String PASSWORD_PARAMETER = "password"; + + private static final Logger log = LoggerFactory.getLogger(AuthorizationChallenge.class); + private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class); + + /** + * Return true if Authorization Challenge Endpoint should return code successfully or otherwise false if error should be returned. + *

+ * Implementation of this method should consist of 3 main parts: + * 1. validate all parameters are present and if not -> set error and return false + * 2. main authorization logic, if ok -> set authorized user into "context.getExecutionContext().setUser(user);" and return true + * 3. if not ok -> set error which explains what is wrong and return false + * + * @param scriptContext ExternalScriptContext, see https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java + * @return true if Authorization Challenge Endpoint should return code successfully or otherwise false if error should be returned. + */ + @Override + public boolean authorize(Object scriptContext) { + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + + // 1. validate all required parameters are present + final String username = getParameterOrCreateError(context, USERNAME_PARAMETER); + if (StringUtils.isBlank(username)) { + return false; + } + + final String password = getParameterOrCreateError(context, PASSWORD_PARAMETER); + if (StringUtils.isBlank(password)) { + return false; + } + + scriptLogger.trace("All required parameters are present"); + + // 2. main authorization logic, if ok -> set authorized user into "context.getExecutionContext().setUser(user);" and return true + UserService userService = CdiUtil.bean(UserService.class); + PersistenceEntryManager entryManager = CdiUtil.bean(PersistenceEntryManager.class); + + final User user = userService.getUser(username); + if (user == null) { + scriptLogger.trace("User is not found by username {}", username); + createError(context, "username_invalid"); + return false; + } + + final boolean ok = entryManager.authenticate(user.getDn(), User.class, password); + if (ok) { + context.getExecutionContext().setUser(user); // <- IMPORTANT : without user set, user relation will not be associated with token + scriptLogger.trace("User {} is authenticated successfully.", username); + return true; + } + + // 3. not ok -> set error which explains what is wrong and return false + scriptLogger.trace("Failed to authenticate user {}. Please check username and password.", username); + createError(context, "username_or_password_invalid"); + return false; + } + + private String getParameterOrCreateError(ExternalScriptContext context, String parameterName) { + String value = context.getHttpRequest().getParameter(parameterName); + + if (StringUtils.isBlank(value)) { + scriptLogger.trace("No '{}' parameter in request", parameterName); + value = getParameterFromDeviceSession(context, parameterName); + } + + if (StringUtils.isBlank(value)) { + scriptLogger.trace("{} is not provided", parameterName); + createError(context, String.format("%s_required", parameterName)); + return null; + } + + return value; + } + + private void createError(ExternalScriptContext context, String errorCode) { + String deviceSessionPart = prepareDeviceSessionSubJson(context); + + final String entity = String.format("{\"error\": \"%s\"%s}", errorCode, deviceSessionPart); + context.createWebApplicationException(401, entity); + } + + private String prepareDeviceSessionSubJson(ExternalScriptContext context) { + DeviceSession deviceSessionObject = context.getAuthzRequest().getDeviceSessionObject(); + if (deviceSessionObject != null) { + prepareDeviceSession(context, deviceSessionObject); + return String.format(",\"device_session\":\"%s\"", deviceSessionObject.getId()); + } else if (context.getAuthzRequest().isUseDeviceSession()) { + deviceSessionObject = prepareDeviceSession(context, null); + return String.format(",\"device_session\":\"%s\"", deviceSessionObject.getId()); + } + return ""; + } + + private DeviceSession prepareDeviceSession(ExternalScriptContext context, DeviceSession deviceSessionObject) { + DeviceSessionService deviceSessionService = CdiUtil.bean(DeviceSessionService.class); + boolean newSave = deviceSessionObject == null; + if (newSave) { + final String id = UUID.randomUUID().toString(); + deviceSessionObject = new DeviceSession(); + deviceSessionObject.setId(id); + deviceSessionObject.setDn(deviceSessionService.buildDn(id)); + } + + String username = context.getHttpRequest().getParameter(USERNAME_PARAMETER); + if (StringUtils.isNotBlank(username)) { + deviceSessionObject.getAttributes().getAttributes().put(USERNAME_PARAMETER, username); + } + + String password = context.getHttpRequest().getParameter(PASSWORD_PARAMETER); + if (StringUtils.isNotBlank(password)) { + deviceSessionObject.getAttributes().getAttributes().put(PASSWORD_PARAMETER, password); + } + + if (newSave) { + deviceSessionService.persist(deviceSessionObject); + } else { + deviceSessionService.merge(deviceSessionObject); + } + + return deviceSessionObject; + } + + private String getParameterFromDeviceSession(ExternalScriptContext context, String parameterName) { + final DeviceSession deviceSessionObject = context.getAuthzRequest().getDeviceSessionObject(); + if (deviceSessionObject != null) { + return deviceSessionObject.getAttributes().getAttributes().get(parameterName); + } + return null; + } + + @Override + public boolean init(Map configurationAttributes) { + scriptLogger.info("Initialized Default AuthorizationChallenge Java custom script."); + return true; + } + + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + scriptLogger.info("Initialized Default AuthorizationChallenge Java custom script."); + return true; + } + + @Override + public boolean destroy(Map configurationAttributes) { + scriptLogger.info("Destroyed Default AuthorizationChallenge Java custom script."); + return true; + } + + @Override + public int getApiVersion() { + return 11; + } +} + +``` + +### Sample Scripts +- [AuthorizationChallenge](../../../script-catalog/authorization_challenge/AuthorizationChallenge.java) \ No newline at end of file diff --git a/docs/script-catalog/authorization_challenge/AuthorizationChallenge.java b/docs/script-catalog/authorization_challenge/AuthorizationChallenge.java new file mode 100644 index 00000000000..02cb8783b8e --- /dev/null +++ b/docs/script-catalog/authorization_challenge/AuthorizationChallenge.java @@ -0,0 +1,177 @@ +import io.jans.as.common.model.common.User; +import io.jans.as.common.model.session.DeviceSession; +import io.jans.as.server.authorize.ws.rs.DeviceSessionService; +import io.jans.as.server.service.UserService; +import io.jans.as.server.service.external.context.ExternalScriptContext; +import io.jans.model.SimpleCustomProperty; +import io.jans.model.custom.script.model.CustomScript; +import io.jans.model.custom.script.type.authzchallenge.AuthorizationChallengeType; +import io.jans.orm.PersistenceEntryManager; +import io.jans.service.cdi.util.CdiUtil; +import io.jans.service.custom.script.CustomScriptManager; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.UUID; + +/** + * @author Yuriy Z + */ +public class AuthorizationChallenge implements AuthorizationChallengeType { + + public static final String USERNAME_PARAMETER = "username"; + public static final String PASSWORD_PARAMETER = "password"; + + private static final Logger log = LoggerFactory.getLogger(AuthorizationChallenge.class); + private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class); + + /** + * Return true if Authorization Challenge Endpoint should return code successfully or otherwise false if error should be returned. + *

+ * Implementation of this method should consist of 3 main parts: + * 1. validate all parameters are present and if not -> set error and return false + * 2. main authorization logic, if ok -> set authorized user into "context.getExecutionContext().setUser(user);" and return true + * 3. if not ok -> set error which explains what is wrong and return false + * + * @param scriptContext ExternalScriptContext, see https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java + * @return true if Authorization Challenge Endpoint should return code successfully or otherwise false if error should be returned. + */ + @Override + public boolean authorize(Object scriptContext) { + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + + // 1. validate all required parameters are present + final String username = getParameterOrCreateError(context, USERNAME_PARAMETER); + if (StringUtils.isBlank(username)) { + return false; + } + + final String password = getParameterOrCreateError(context, PASSWORD_PARAMETER); + if (StringUtils.isBlank(password)) { + return false; + } + + scriptLogger.trace("All required parameters are present"); + + // 2. main authorization logic, if ok -> set authorized user into "context.getExecutionContext().setUser(user);" and return true + UserService userService = CdiUtil.bean(UserService.class); + PersistenceEntryManager entryManager = CdiUtil.bean(PersistenceEntryManager.class); + + final User user = userService.getUser(username); + if (user == null) { + scriptLogger.trace("User is not found by username {}", username); + createError(context, "username_invalid"); + return false; + } + + final boolean ok = entryManager.authenticate(user.getDn(), User.class, password); + if (ok) { + context.getExecutionContext().setUser(user); // <- IMPORTANT : without user set, user relation will not be associated with token + scriptLogger.trace("User {} is authenticated successfully.", username); + return true; + } + + // 3. not ok -> set error which explains what is wrong and return false + scriptLogger.trace("Failed to authenticate user {}. Please check username and password.", username); + createError(context, "username_or_password_invalid"); + return false; + } + + private String getParameterOrCreateError(ExternalScriptContext context, String parameterName) { + String value = context.getHttpRequest().getParameter(parameterName); + + if (StringUtils.isBlank(value)) { + scriptLogger.trace("No '{}' parameter in request", parameterName); + value = getParameterFromDeviceSession(context, parameterName); + } + + if (StringUtils.isBlank(value)) { + scriptLogger.trace("{} is not provided", parameterName); + createError(context, String.format("%s_required", parameterName)); + return null; + } + + return value; + } + + private void createError(ExternalScriptContext context, String errorCode) { + String deviceSessionPart = prepareDeviceSessionSubJson(context); + + final String entity = String.format("{\"error\": \"%s\"%s}", errorCode, deviceSessionPart); + context.createWebApplicationException(401, entity); + } + + private String prepareDeviceSessionSubJson(ExternalScriptContext context) { + DeviceSession deviceSessionObject = context.getAuthzRequest().getDeviceSessionObject(); + if (deviceSessionObject != null) { + prepareDeviceSession(context, deviceSessionObject); + return String.format(",\"device_session\":\"%s\"", deviceSessionObject.getId()); + } else if (context.getAuthzRequest().isUseDeviceSession()) { + deviceSessionObject = prepareDeviceSession(context, null); + return String.format(",\"device_session\":\"%s\"", deviceSessionObject.getId()); + } + return ""; + } + + private DeviceSession prepareDeviceSession(ExternalScriptContext context, DeviceSession deviceSessionObject) { + DeviceSessionService deviceSessionService = CdiUtil.bean(DeviceSessionService.class); + boolean newSave = deviceSessionObject == null; + if (newSave) { + final String id = UUID.randomUUID().toString(); + deviceSessionObject = new DeviceSession(); + deviceSessionObject.setId(id); + deviceSessionObject.setDn(deviceSessionService.buildDn(id)); + } + + String username = context.getHttpRequest().getParameter(USERNAME_PARAMETER); + if (StringUtils.isNotBlank(username)) { + deviceSessionObject.getAttributes().getAttributes().put(USERNAME_PARAMETER, username); + } + + String password = context.getHttpRequest().getParameter(PASSWORD_PARAMETER); + if (StringUtils.isNotBlank(password)) { + deviceSessionObject.getAttributes().getAttributes().put(PASSWORD_PARAMETER, password); + } + + if (newSave) { + deviceSessionService.persist(deviceSessionObject); + } else { + deviceSessionService.merge(deviceSessionObject); + } + + return deviceSessionObject; + } + + private String getParameterFromDeviceSession(ExternalScriptContext context, String parameterName) { + final DeviceSession deviceSessionObject = context.getAuthzRequest().getDeviceSessionObject(); + if (deviceSessionObject != null) { + return deviceSessionObject.getAttributes().getAttributes().get(parameterName); + } + return null; + } + + @Override + public boolean init(Map configurationAttributes) { + scriptLogger.info("Initialized Default AuthorizationChallenge Java custom script."); + return true; + } + + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + scriptLogger.info("Initialized Default AuthorizationChallenge Java custom script."); + return true; + } + + @Override + public boolean destroy(Map configurationAttributes) { + scriptLogger.info("Destroyed Default AuthorizationChallenge Java custom script."); + return true; + } + + @Override + public int getApiVersion() { + return 11; + } +} \ No newline at end of file diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizationResponse.java b/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizationResponse.java index 46c77d6d9e6..cdf1e7a4def 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizationResponse.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizationResponse.java @@ -63,6 +63,7 @@ public class AuthorizationResponse extends BaseResponse { protected Integer exp; private AuthorizeErrorResponseType errorType; + private String errorTypeString; private String errorDescription; private String errorUri; @@ -81,7 +82,8 @@ public AuthorizationResponse(Response clientResponse) { try { JSONObject jsonObj = new JSONObject(entity); if (jsonObj.has(Constants.ERROR)) { - errorType = AuthorizeErrorResponseType.fromString(jsonObj.getString(Constants.ERROR)); + errorTypeString = jsonObj.getString(Constants.ERROR); + errorType = AuthorizeErrorResponseType.fromString(errorTypeString); } if (jsonObj.has(Constants.ERROR_DESCRIPTION)) { errorDescription = jsonObj.getString(Constants.ERROR_DESCRIPTION); @@ -95,6 +97,9 @@ public AuthorizationResponse(Response clientResponse) { if (jsonObj.has(Constants.REDIRECT)) { location = jsonObj.getString(Constants.REDIRECT); } + if (jsonObj.has(Constants.AUTHORIZATION_CODE)) { + code = jsonObj.getString(Constants.AUTHORIZATION_CODE); + } } catch (JSONException e) { LOG.error(e.getMessage(), e); } @@ -268,6 +273,13 @@ private void loadParams(Map params) throws UnsupportedEncodingEx } } + public String getErrorTypeString() { + return errorTypeString; + } + + public void setErrorTypeString(String errorTypeString) { + this.errorTypeString = errorTypeString; + } /** * Returns the authorization code generated by the authorization server. diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationClient.java b/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationClient.java index c56181d402c..fdab0cd1a8b 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationClient.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationClient.java @@ -104,6 +104,7 @@ public static void parse(String json, OpenIdConfigurationResponse response) { response.setIssuer(jsonObj.optString(ISSUER, null)); response.setAuthorizationEndpoint(jsonObj.optString(AUTHORIZATION_ENDPOINT, null)); + response.setAuthorizationChallengeEndpoint(jsonObj.optString(AUTHORIZATION_CHALLENGE_ENDPOINT, null)); response.setTokenEndpoint(jsonObj.optString(TOKEN_ENDPOINT, null)); response.setRevocationEndpoint(jsonObj.optString(REVOCATION_ENDPOINT, null)); response.setSessionRevocationEndpoint(jsonObj.optString(SESSION_REVOCATION_ENDPOINT, null)); diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationResponse.java b/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationResponse.java index 799954b8d49..875f2abe2e7 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationResponse.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationResponse.java @@ -27,6 +27,7 @@ public class OpenIdConfigurationResponse extends BaseResponse implements Seriali private String issuer; private String authorizationEndpoint; + private String authorizationChallengeEndpoint; private String tokenEndpoint; private String revocationEndpoint; private String sessionRevocationEndpoint; @@ -214,6 +215,24 @@ public void setAuthorizationEndpoint(String authorizationEndpoint) { this.authorizationEndpoint = authorizationEndpoint; } + /** + * Returns the URL of the Authorization Challenge Endpoint. + * + * @return The URL of the Authorization Challenge Endpoint. + */ + public String getAuthorizationChallengeEndpoint() { + return authorizationChallengeEndpoint; + } + + /** + * Sets Authorization Challenge Endpoint. + * + * @param authorizationChallengeEndpoint Authorization Challenge Endpoint + */ + public void setAuthorizationChallengeEndpoint(String authorizationChallengeEndpoint) { + this.authorizationChallengeEndpoint = authorizationChallengeEndpoint; + } + /** * Returns the URL of the Token endpoint. * @@ -1202,6 +1221,7 @@ public String toString() { return "OpenIdConfigurationResponse{" + "issuer='" + issuer + '\'' + ", authorizationEndpoint='" + authorizationEndpoint + '\'' + + ", authorizationChallengeEndpoint='" + authorizationChallengeEndpoint + '\'' + ", tokenEndpoint='" + tokenEndpoint + '\'' + ", revocationEndpoint='" + revocationEndpoint + '\'' + ", userInfoEndpoint='" + userInfoEndpoint + '\'' + diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/BaseTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/BaseTest.java index 02504287c24..d31227a26ad 100644 --- a/jans-auth-server/client/src/test/java/io/jans/as/client/BaseTest.java +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/BaseTest.java @@ -89,6 +89,7 @@ public abstract class BaseTest { protected HtmlUnitDriver driver; protected String authorizationEndpoint; + protected String authorizationChallengeEndpoint; protected String authorizationPageEndpoint; protected String gluuConfigurationEndpoint; protected String tokenEndpoint; @@ -293,6 +294,14 @@ public void setAuthorizationEndpoint(String authorizationEndpoint) { this.authorizationEndpoint = authorizationEndpoint; } + public String getAuthorizationChallengeEndpoint() { + return authorizationChallengeEndpoint; + } + + public void setAuthorizationChallengeEndpoint(String authorizationChallengeEndpoint) { + this.authorizationChallengeEndpoint = authorizationChallengeEndpoint; + } + public String getTokenEndpoint() { return tokenEndpoint; } @@ -971,6 +980,7 @@ public void discovery(ITestContext context) throws Exception { Asserter.assertOpenIdConfigurationResponse(response); authorizationEndpoint = response.getAuthorizationEndpoint(); + authorizationChallengeEndpoint = response.getAuthorizationChallengeEndpoint(); tokenEndpoint = response.getTokenEndpoint(); tokenRevocationEndpoint = response.getRevocationEndpoint(); userInfoEndpoint = response.getUserInfoEndpoint(); @@ -992,6 +1002,7 @@ public void discovery(ITestContext context) throws Exception { showTitle("Loading configuration endpoints from properties file"); authorizationEndpoint = context.getCurrentXmlTest().getParameter("authorizationEndpoint"); + authorizationChallengeEndpoint = context.getCurrentXmlTest().getParameter("authorizationChallengeEndpoint"); tokenEndpoint = context.getCurrentXmlTest().getParameter("tokenEndpoint"); tokenRevocationEndpoint = context.getCurrentXmlTest().getParameter("tokenRevocationEndpoint"); userInfoEndpoint = context.getCurrentXmlTest().getParameter("userInfoEndpoint"); diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/ciba/ConfigurationTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/ciba/ConfigurationTest.java index deaf32e7b84..73c448fca57 100644 --- a/jans-auth-server/client/src/test/java/io/jans/as/client/ciba/ConfigurationTest.java +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/ciba/ConfigurationTest.java @@ -53,6 +53,7 @@ public void requestOpenIdConfiguration(final String resource) throws Exception { assertEquals(response.getStatus(), 200, "Unexpected response code"); assertNotNull(response.getIssuer(), "The issuer is null"); assertNotNull(response.getAuthorizationEndpoint(), "The authorizationEndpoint is null"); + assertNotNull(response.getAuthorizationChallengeEndpoint(), "The authorizationChallengeEndpoint is null"); assertNotNull(response.getTokenEndpoint(), "The tokenEndpoint is null"); assertNotNull(response.getRevocationEndpoint(), "The tokenRevocationEndpoint is null"); assertNotNull(response.getUserInfoEndpoint(), "The userInfoEndPoint is null"); diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/client/Asserter.java b/jans-auth-server/client/src/test/java/io/jans/as/client/client/Asserter.java index 0e12409d355..e0664e83c13 100644 --- a/jans-auth-server/client/src/test/java/io/jans/as/client/client/Asserter.java +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/client/Asserter.java @@ -71,6 +71,7 @@ public static void assertOpenIdConfigurationResponse(OpenIdConfigurationResponse assertEquals(response.getStatus(), 200, "Unexpected response code"); assertNotNull(response.getIssuer(), "The issuer is null"); assertNotNull(response.getAuthorizationEndpoint(), "The authorizationEndpoint is null"); + assertNotNull(response.getAuthorizationChallengeEndpoint(), "The authorizationChallengeEndpoint is null"); assertNotNull(response.getTokenEndpoint(), "The tokenEndpoint is null"); assertNotNull(response.getRevocationEndpoint(), "The tokenRevocationEndpoint is null"); assertNotNull(response.getUserInfoEndpoint(), "The userInfoEndPoint is null"); diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/AuthorizationChallengeHttpTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/AuthorizationChallengeHttpTest.java new file mode 100644 index 00000000000..612a083696d --- /dev/null +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/AuthorizationChallengeHttpTest.java @@ -0,0 +1,188 @@ +package io.jans.as.client.ws.rs; + +import com.google.common.collect.Lists; +import io.jans.as.client.*; +import io.jans.as.client.client.AssertBuilder; +import io.jans.as.model.common.*; +import io.jans.as.model.crypto.signature.SignatureAlgorithm; +import io.jans.as.model.jwt.JwtClaimName; +import io.jans.as.model.register.ApplicationType; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.AssertJUnit.assertNotNull; + +/** + * Authorization Challenge Endpoint HTTP Test + * + * @author Yuriy Z + */ +public class AuthorizationChallengeHttpTest extends BaseTest { + + /** + * Test for the complete Authorization Code Flow. + */ + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri"}) + @Test + public void authorizationChallengeFlow( + final String userId, final String userSecret, final String redirectUris, final String redirectUri) throws Exception { + showTitle("authorizationChallengeFlow"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List grantTypes = Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN); + + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, grantTypes, scopes); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization code at Authorization Challenge Endpoint + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(null); + authorizationRequest.setClientId(clientId); + authorizationRequest.setAcrValues(Lists.newArrayList()); + authorizationRequest.setScopes(scopes); + authorizationRequest.setNonce(nonce); + authorizationRequest.setState(state); + authorizationRequest.addCustomParameter("username", userId); + authorizationRequest.addCustomParameter("password", userSecret); + authorizationRequest.setAuthorizationMethod(AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationChallengeEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + showClient(authorizeClient); + assertNotNull(authorizationResponse); + + String authorizationCode = authorizationResponse.getCode(); + assertNotNull(authorizationCode); + System.out.println(String.format("Successfully obtained authorization code %s at Authorization Challenge Endpoint", authorizationCode)); + + // 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); + AssertBuilder.tokenResponse(tokenResponse1) + .notNullRefreshToken() + .check(); + + String refreshToken = tokenResponse1.getRefreshToken(); + + // 4. Validate id_token + AssertBuilder.jwtParse(tokenResponse1.getIdToken()) + .validateSignatureRSAClientEngine(jwksUri, SignatureAlgorithm.RS256) + .claimsPresence(JwtClaimName.CODE_HASH) + .notNullAuthenticationTime() + .notNullJansOpenIDConnectVersion() + .notNullAuthenticationContextClassReference() + .notNullAuthenticationMethodReferences() + .check(); + + // 5. Request new access token using the refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + tokenClient2.setExecutor(clientEngine(true)); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(tokenResponse1.getScope(), refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + AssertBuilder.tokenResponse(tokenResponse2) + .notNullRefreshToken() + .notNullScope() + .check(); + String accessToken = tokenResponse2.getAccessToken(); + + // 6. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setExecutor(clientEngine(true)); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + AssertBuilder.userInfoResponse(userInfoResponse) + .check(); + } + + /** + * Test for the complete Authorization Code Flow. + */ + @Parameters({"userSecret", "redirectUris", "redirectUri"}) + @Test + public void authorizationChallengeFlow_withInvalidUsername_shouldGetError( + final String userSecret, final String redirectUris, final String redirectUri) { + showTitle("authorizationChallengeFlow"); + + String userId = "invalidUser"; + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List grantTypes = Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN); + + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, grantTypes, scopes); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization code at Authorization Challenge Endpoint + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(null); + authorizationRequest.setClientId(clientId); + authorizationRequest.setAcrValues(Lists.newArrayList()); + authorizationRequest.setScopes(scopes); + authorizationRequest.setNonce(nonce); + authorizationRequest.setState(state); + authorizationRequest.addCustomParameter("username", userId); + authorizationRequest.addCustomParameter("password", userSecret); + authorizationRequest.setAuthorizationMethod(AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationChallengeEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + showClient(authorizeClient); + assertNotNull(authorizationResponse); + assertNull(authorizationResponse.getCode()); + assertEquals(authorizationResponse.getErrorTypeString(), "username_invalid"); + } + + public RegisterResponse registerClient(final String redirectUris, List responseTypes, List grantTypes, List scopes) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "jans test app", + io.jans.as.model.util.StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setSubjectType(SubjectType.PUBLIC); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + AssertBuilder.registerResponse(registerResponse).created().check(); + return registerResponse; + } +} diff --git a/jans-auth-server/client/src/test/resources/testng.xml b/jans-auth-server/client/src/test/resources/testng.xml index fe92312ceb8..bc405f9501f 100644 --- a/jans-auth-server/client/src/test/resources/testng.xml +++ b/jans-auth-server/client/src/test/resources/testng.xml @@ -26,7 +26,11 @@ - + + + + + diff --git a/jans-auth-server/common/src/main/java/io/jans/as/common/model/session/DeviceSession.java b/jans-auth-server/common/src/main/java/io/jans/as/common/model/session/DeviceSession.java new file mode 100644 index 00000000000..2d16a59e3e5 --- /dev/null +++ b/jans-auth-server/common/src/main/java/io/jans/as/common/model/session/DeviceSession.java @@ -0,0 +1,110 @@ +package io.jans.as.common.model.session; + +import io.jans.orm.annotation.*; +import io.jans.orm.model.base.DeletableEntity; + +import java.io.Serializable; +import java.util.Date; + +/** + * @author Yuriy Z + */ +@DataEntry +@ObjectClass(value = "jansDeviceSess") +public class DeviceSession extends DeletableEntity implements Serializable { + + @DN + private String dn; + + @AttributeName(name = "jansId") + private String id; + + @AttributeName(name = "jansUsrDN") + private String userDn; + + @AttributeName(name = "creationDate") + private Date creationDate = new Date(); + + @AttributeName(name = "clnId") + private String clientId; + + @AttributeName(name = "attr") + @JsonObject + private DeviceSessionAttributes attributes; + + @Expiration + private int ttl; + + + @Override + public String getDn() { + return dn; + } + + @Override + public void setDn(String dn) { + this.dn = dn; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUserDn() { + return userDn; + } + + public void setUserDn(String userDn) { + this.userDn = userDn; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public DeviceSessionAttributes getAttributes() { + if (attributes == null) attributes = new DeviceSessionAttributes(); + return attributes; + } + + public void setAttributes(DeviceSessionAttributes attributes) { + this.attributes = attributes; + } + + public int getTtl() { + return ttl; + } + + public void setTtl(int ttl) { + this.ttl = ttl; + } + + @Override + public String toString() { + return "DeviceSession{" + + "dn='" + dn + '\'' + + ", id='" + id + '\'' + + ", userDn='" + userDn + '\'' + + ", creationDate=" + creationDate + + ", clientId='" + clientId + '\'' + + ", attributes=" + attributes + + ", ttl=" + ttl + + "} " + super.toString(); + } +} diff --git a/jans-auth-server/common/src/main/java/io/jans/as/common/model/session/DeviceSessionAttributes.java b/jans-auth-server/common/src/main/java/io/jans/as/common/model/session/DeviceSessionAttributes.java new file mode 100644 index 00000000000..9aad344289c --- /dev/null +++ b/jans-auth-server/common/src/main/java/io/jans/as/common/model/session/DeviceSessionAttributes.java @@ -0,0 +1,48 @@ +package io.jans.as.common.model.session; + +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 Z + */ +@JsonIgnoreProperties( + ignoreUnknown = true +) +public class DeviceSessionAttributes implements Serializable { + + @JsonProperty("acr_values") + private String acrValues; + + @JsonProperty("attributes") + private Map attributes; + + public Map getAttributes() { + if (attributes == null) attributes = new HashMap<>(); + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + public String getAcrValues() { + return acrValues; + } + + public void setAcrValues(String acrValues) { + this.acrValues = acrValues; + } + + @Override + public String toString() { + return "DeviceSessionAttributes{" + + "acrValues='" + acrValues + '\'' + + "attributes='" + attributes + '\'' + + '}'; + } +} diff --git a/jans-auth-server/docs/swagger.yaml b/jans-auth-server/docs/swagger.yaml index 80df685f844..26ceac5138b 100644 --- a/jans-auth-server/docs/swagger.yaml +++ b/jans-auth-server/docs/swagger.yaml @@ -18,6 +18,84 @@ tags: description: Janssen Authorization Server 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. Server 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. Server is tightly coupled with Gluu Admin UI. paths: + /authorization_challenge: + post: + tags: + - Authorization Challenge + summary: The Authorization Challenge Endpoint performs Authentication of the End-User by native application to obtain an authorization code. + description: The Authorization Challenge Endpoint performs Authentication of the End-User by native application to obtain an authorization code. + operationId: post_authorize_challenge + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - client_id + properties: + client_id: + type: string + description: OAuth 2.0 Client Identifier valid at the Authorization Server. + scope: + type: string + description: OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. + device_session: + type: string + description: If the client has previously obtained a device session + use_device_session: + type: boolean + description: If return back in response device_session. By default AS does not return device_session + state: + type: string + description: Opaque value used to maintain state between the request and the callback. + nonce: + type: string + description: String value used to associate a Client session with an ID Token, and to mitigate replay attacks. + login_hint: + type: string + description: Hint to the Authorization Server about the login identifier the End-User might use to log in (if necessary). + acr_values: + type: string + description: 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. + amr_values: + type: string + description: AMR Values. + request_session_id: + type: string + description: Request session id. + session_id: + type: string + description: Session id of this call. + origin_headers: + type: string + description: Origin headers. Used in custom workflows. + custom_response_headers: + type: string + description: Custom Response Headers. + responses: + 200: + description: OK + content: + application/json: + schema: + title: AuthorizationChallengeResponse + description: Authorization Challenge Response + required: + - authorization_code + type: object + properties: + authorization_code: + type: string + description: Authorization Code + example: uY29tL2F1dGhlbnRpY + 400: + $ref: '#/components/responses/InvalidRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/InternalServerError' + /authorize: get: tags: diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizationChallengeResponse.java b/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizationChallengeResponse.java new file mode 100644 index 00000000000..4f1752b96cd --- /dev/null +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizationChallengeResponse.java @@ -0,0 +1,44 @@ +package io.jans.as.model.authorize; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Yuriy Z + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AuthorizationChallengeResponse { + + @JsonProperty(value = "authorization_code") + private String authorizationCode; + + /** + * Gets authorization code + * + * @return authorization code + */ + public String getAuthorizationCode() { + return authorizationCode; + } + + /** + * Sets authorization code + * + * @param authorizationCode authorization code + */ + public void setAuthorizationCode(String authorizationCode) { + this.authorizationCode = authorizationCode; + } + + /** + * Returns string representation of authorization challenge response + * + * @return string representation of authorization challenge response + */ + @Override + public String toString() { + return "AuthorizationChallengeResponse{" + + "authorizationCode='" + authorizationCode + '\'' + + '}'; + } +} diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/config/Constants.java b/jans-auth-server/model/src/main/java/io/jans/as/model/config/Constants.java index bff46cf9415..aa31ce914ae 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/config/Constants.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/config/Constants.java @@ -38,6 +38,7 @@ private Constants() { public static final String CLIENT_ASSERTION_TYPE = "client_assertion_type"; public static final String CLIENT_ID = "client_id"; public static final String DEVICE_AUTHORIZATION = "device_authorization"; + public static final String AUTHORIZATION_CODE = "authorization_code"; public static final String LOG_FOUND = "Found '{}' entries"; diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java index cfa7c50d27d..6f1034b4d15 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java @@ -33,6 +33,7 @@ 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"; + public static final String DEFAULT_AUTHORIZATION_CHALLENGE_ACR = "default_challenge"; @DocProperty(description = "URL using the https scheme that OP asserts as Issuer identifier") private String issuer; @@ -43,6 +44,9 @@ public class AppConfiguration implements Configuration { @DocProperty(description = "The authorization endpoint URL") private String authorizationEndpoint; + @DocProperty(description = "The authorization challenge endpoint URL") + private String authorizationChallengeEndpoint; + @DocProperty(description = "The token endpoint URL") private String tokenEndpoint; @@ -91,6 +95,9 @@ public class AppConfiguration implements Configuration { @DocProperty(description = "URL for Mutual TLS (mTLS) Client Authentication and Certificate-Bound Access Tokens (MTLS) Endpoint") private String mtlsAuthorizationEndpoint; + @DocProperty(description = "URL for Mutual TLS (mTLS) Client Authentication and Certificate-Bound Access Tokens (MTLS) Authorization Challenge Endpoint") + private String mtlsAuthorizationChallengeEndpoint; + @DocProperty(description = "URL for Mutual TLS (mTLS) Authorization token Endpoint") private String mtlsTokenEndpoint; @@ -839,6 +846,12 @@ public class AppConfiguration implements Configuration { @DocProperty(description = "Enable/Disable block authorizations that originate from Webview (Mobile apps).", defaultValue = "false") private Boolean blockWebviewAuthorizationEnabled = false; + @DocProperty(description = "Authorization Challenge Endpoint Default ACR if no value is specified in acr_values request parameter.", defaultValue = DEFAULT_AUTHORIZATION_CHALLENGE_ACR) + private String authorizationChallengeDefaultAcr; + + @DocProperty(description = "Boolean value specifying whether to generate session_id (AS object and cookie) during authorization at Authorization Challenge Endpoint", defaultValue = "false") + private Boolean authorizationChallengeShouldGenerateSession = false; + @DocProperty(description = "List of key value date formatters, e.g. 'userinfo: 'yyyy-MM-dd', etc.") private Map dateFormatterPatterns = new HashMap<>(); @@ -1503,6 +1516,24 @@ public void setAuthorizationEndpoint(String authorizationEndpoint) { this.authorizationEndpoint = authorizationEndpoint; } + /** + * Gets authorization challenge endpoint. + * + * @return authorization challenge endpoint + */ + public String getAuthorizationChallengeEndpoint() { + return authorizationChallengeEndpoint; + } + + /** + * Sets authorization challenge endpoint + * + * @param authorizationChallengeEndpoint authorization challenge endpoint + */ + public void setAuthorizationChallengeEndpoint(String authorizationChallengeEndpoint) { + this.authorizationChallengeEndpoint = authorizationChallengeEndpoint; + } + /** * Returns the URL of the Token endpoint. * @@ -3075,6 +3106,24 @@ public void setMtlsAuthorizationEndpoint(String mtlsAuthorizationEndpoint) { this.mtlsAuthorizationEndpoint = mtlsAuthorizationEndpoint; } + /** + * Gets MTLS Authorization Challenge Endpoint. + * + * @return MTLS Authorization Challenge Endpoint. + */ + public String getMtlsAuthorizationChallengeEndpoint() { + return mtlsAuthorizationChallengeEndpoint; + } + + /** + * Sets MTLS Authorization Challenge Endpoint. + * + * @param mtlsAuthorizationChallengeEndpoint MTLS Authorization Challenge Endpoint. + */ + public void setMtlsAuthorizationChallengeEndpoint(String mtlsAuthorizationChallengeEndpoint) { + this.mtlsAuthorizationChallengeEndpoint = mtlsAuthorizationChallengeEndpoint; + } + public String getMtlsTokenEndpoint() { return mtlsTokenEndpoint; } @@ -3228,6 +3277,24 @@ public void setSsaConfiguration(SsaConfiguration ssaConfiguration) { this.ssaConfiguration = ssaConfiguration; } + public Boolean getAuthorizationChallengeShouldGenerateSession() { + if (authorizationChallengeShouldGenerateSession == null) authorizationChallengeShouldGenerateSession = false; + return authorizationChallengeShouldGenerateSession; + } + + public void setAuthorizationChallengeShouldGenerateSession(Boolean authorizationChallengeShouldGenerateSession) { + this.authorizationChallengeShouldGenerateSession = authorizationChallengeShouldGenerateSession; + } + + public String getAuthorizationChallengeDefaultAcr() { + if (authorizationChallengeDefaultAcr == null) authorizationChallengeDefaultAcr = DEFAULT_AUTHORIZATION_CHALLENGE_ACR; + return authorizationChallengeDefaultAcr; + } + + public void setAuthorizationChallengeDefaultAcr(String authorizationChallengeDefaultAcr) { + this.authorizationChallengeDefaultAcr = authorizationChallengeDefaultAcr; + } + public Boolean getBlockWebviewAuthorizationEnabled() { return blockWebviewAuthorizationEnabled; } diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/ConfigurationResponseClaim.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/ConfigurationResponseClaim.java index fa3b70ba357..f35461bb71f 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/ConfigurationResponseClaim.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/ConfigurationResponseClaim.java @@ -17,6 +17,7 @@ private ConfigurationResponseClaim() { public static final String ISSUER = "issuer"; public static final String AUTHORIZATION_ENDPOINT = "authorization_endpoint"; + public static final String AUTHORIZATION_CHALLENGE_ENDPOINT = "authorization_challenge_endpoint"; public static final String TOKEN_ENDPOINT = "token_endpoint"; public static final String REVOCATION_ENDPOINT = "revocation_endpoint"; public static final String SESSION_REVOCATION_ENDPOINT = "session_revocation_endpoint"; diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/discovery/OAuth2Discovery.java b/jans-auth-server/model/src/main/java/io/jans/as/model/discovery/OAuth2Discovery.java index 17645efad0b..69c84cd89ba 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/discovery/OAuth2Discovery.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/discovery/OAuth2Discovery.java @@ -57,6 +57,10 @@ public class OAuth2Discovery { @XmlElement(name = "authorization_endpoint") private String authorizationEndpoint; + @JsonProperty(value = "authorization_challenge_endpoint") + @XmlElement(name = "authorization_challenge_endpoint") + private String authorizationChallengeEndpoint; + @JsonProperty(value = "token_endpoint") @XmlElement(name = "token_endpoint") private String tokenEndpoint; @@ -125,6 +129,14 @@ public void setAuthorizationEndpoint(String authorizationEndpoint) { this.authorizationEndpoint = authorizationEndpoint; } + public String getAuthorizationChallengeEndpoint() { + return authorizationChallengeEndpoint; + } + + public void setAuthorizationChallengeEndpoint(String authorizationChallengeEndpoint) { + this.authorizationChallengeEndpoint = authorizationChallengeEndpoint; + } + public String getTokenEndpoint() { return tokenEndpoint; } @@ -234,6 +246,7 @@ public String toString() { return "OAuth2Discovery{" + "issuer='" + issuer + '\'' + ", authorizationEndpoint='" + authorizationEndpoint + '\'' + + ", authorizationChallengeEndpoint='" + authorizationChallengeEndpoint + '\'' + ", tokenEndpoint='" + tokenEndpoint + '\'' + ", jwksUri='" + jwksUri + '\'' + ", registrationEndpoint='" + registrationEndpoint + '\'' + diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/error/ErrorResponseFactory.java b/jans-auth-server/model/src/main/java/io/jans/as/model/error/ErrorResponseFactory.java index d1f03268146..b873c1243c8 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/error/ErrorResponseFactory.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/error/ErrorResponseFactory.java @@ -24,6 +24,10 @@ import io.jans.as.model.uma.UmaErrorResponseType; import io.jans.as.model.userinfo.UserInfoErrorResponseType; import io.jans.as.model.util.Util; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.apache.commons.lang.BooleanUtils; import org.apache.logging.log4j.ThreadContext; import org.jetbrains.annotations.NotNull; @@ -31,9 +35,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import java.io.IOException; import java.util.List; import java.util.Optional; @@ -122,6 +123,15 @@ public void validateFeatureEnabled(FeatureFlagType flagType) { .build()); } + public Response.ResponseBuilder newErrorResponse(Response.Status status) { + final CacheControl cacheControl = new CacheControl(); + cacheControl.setNoStore(true); + + return Response.status(status) + .cacheControl(cacheControl) + .type(MediaType.APPLICATION_JSON_TYPE); + } + public WebApplicationException createWebApplicationException(Response.Status status, IErrorType type, String reason) throws WebApplicationException { return new WebApplicationException(Response .status(status) diff --git a/jans-auth-server/server/conf/jans-config.json b/jans-auth-server/server/conf/jans-config.json index 2629fe5b659..64dcd54eb57 100644 --- a/jans-auth-server/server/conf/jans-config.json +++ b/jans-auth-server/server/conf/jans-config.json @@ -4,6 +4,7 @@ "authorizationPage":"${config.oxauth.contextPath}/authorize.htm", "baseEndpoint":"${config.oxauth.contextPath}/restv1", "authorizationEndpoint":"${config.oxauth.contextPath}/restv1/authorize", + "authorizationChallengeEndpoint":"${config.oxauth.contextPath}/restv1/authorization_challenge", "tokenEndpoint":"${config.oxauth.contextPath}/restv1/token", "tokenRevocationEndpoint": "${config.oxauth.contextPath}/restv1/revoke", "userInfoEndpoint":"${config.oxauth.contextPath}/restv1/userinfo", diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeEndpoint.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeEndpoint.java new file mode 100644 index 00000000000..7ba528bb654 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeEndpoint.java @@ -0,0 +1,63 @@ +package io.jans.as.server.authorize.ws.rs; + +import io.jans.as.model.util.QueryStringDecoder; +import io.jans.as.server.service.RequestParameterService; +import jakarta.inject.Inject; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * The authorization challenge endpoint is a new endpoint defined by "OAuth 2.0 for First-Party Native Applications" + * specification which the native application uses to obtain an authorization code. + * The endpoint accepts the authorization request parameters defined in [RFC6749] for the authorization endpoint + * as well as all applicable extensions defined for the authorization endpoint. Some examples of such extensions + * include Proof Key for Code Exchange (PKCE) [RFC7636], Resource Indicators [RFC8707], and OpenID Connect [OpenID]. + * It is important to note that some extension parameters have meaning in a web context but don't have meaning in + * a native mechanism (e.g. response_mode=query). + * + * @author Yuriy Z + */ +@Path("/authorization_challenge") +public class AuthorizationChallengeEndpoint { + + @Inject + private RequestParameterService requestParameterService; + + @Inject + private AuthorizationChallengeService authorizationChallengeService; + + @POST + @Produces({MediaType.APPLICATION_JSON}) + public Response requestAuthorizationPost( + @FormParam("client_id") String clientId, + @FormParam("scope") String scope, + @FormParam("acr_values") String acrValues, + @FormParam("device_session") String deviceSession, + @FormParam("use_device_session") String useDeviceSession, + @FormParam("prompt") String prompt, + @FormParam("state") String state, + @FormParam("nonce") String nonce, + @Context HttpServletRequest httpRequest, + @Context HttpServletResponse httpResponse) { + + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setHttpMethod(HttpMethod.POST); + authzRequest.setClientId(clientId); + authzRequest.setScope(scope); + authzRequest.setAcrValues(acrValues); + authzRequest.setDeviceSession(deviceSession); + authzRequest.setUseDeviceSession(Boolean.parseBoolean(useDeviceSession)); + authzRequest.setState(state); + authzRequest.setNonce(nonce); + authzRequest.setPrompt(prompt); + authzRequest.setCustomParameters(requestParameterService.getCustomParameters(QueryStringDecoder.decode(httpRequest.getQueryString()))); + authzRequest.setHttpRequest(httpRequest); + authzRequest.setHttpResponse(httpResponse); + + return authorizationChallengeService.requestAuthorization(authzRequest); + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java new file mode 100644 index 00000000000..799e3049f01 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java @@ -0,0 +1,208 @@ +package io.jans.as.server.authorize.ws.rs; + +import com.google.common.collect.Maps; +import io.jans.as.common.model.common.User; +import io.jans.as.common.model.registration.Client; +import io.jans.as.common.model.session.SessionId; +import io.jans.as.model.authorize.AuthorizationChallengeResponse; +import io.jans.as.model.authorize.AuthorizeErrorResponseType; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.crypto.binding.TokenBindingMessage; +import io.jans.as.model.crypto.binding.TokenBindingParseException; +import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.server.audit.ApplicationAuditLogger; +import io.jans.as.server.model.authorize.ScopeChecker; +import io.jans.as.server.model.common.AuthorizationCodeGrant; +import io.jans.as.server.model.common.AuthorizationGrantList; +import io.jans.as.server.model.common.ExecutionContext; +import io.jans.as.server.security.Identity; +import io.jans.as.server.service.CookieService; +import io.jans.as.server.service.RequestParameterService; +import io.jans.as.server.service.SessionIdService; +import io.jans.as.server.service.external.ExternalAuthorizationChallengeService; +import io.jans.as.server.util.ServerUtil; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; + +import java.io.IOException; +import java.util.Date; +import java.util.Map; +import java.util.Set; + +import static io.jans.as.server.authorize.ws.rs.AuthorizeRestWebServiceImpl.getGenericRequestMap; +import static org.apache.commons.lang3.BooleanUtils.isFalse; + +/** + * @author Yuriy Z + */ +@RequestScoped +@Named +public class AuthorizationChallengeService { + + @Inject + private Logger log; + + @Inject + private AuthzRequestService authzRequestService; + + @Inject + private ApplicationAuditLogger applicationAuditLogger; + + @Inject + private AuthorizeRestWebServiceValidator authorizeRestWebServiceValidator; + + @Inject + private ScopeChecker scopeChecker; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private AuthorizationChallengeValidator authorizationChallengeValidator; + + @Inject + private ExternalAuthorizationChallengeService externalAuthorizationChallengeService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private DeviceSessionService deviceSessionService; + + @Inject + private Identity identity; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private RequestParameterService requestParameterService; + + @Inject + private CookieService cookieService; + + public Response requestAuthorization(AuthzRequest authzRequest) { + log.debug("Attempting to request authz challenge: {}", authzRequest); + + authzRequestService.createOauth2AuditLog(authzRequest); + + try { + return authorize(authzRequest); + } catch (WebApplicationException e) { + if (log.isErrorEnabled() && AuthzRequestService.canLogWebApplicationException(e)) + log.error(e.getMessage(), e); + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + } finally { + applicationAuditLogger.sendMessage(authzRequest.getAuditLog()); + } + + return Response.status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()).build(); + } + + public void prepareAuthzRequest(AuthzRequest authzRequest) { + authzRequest.setScope(ServerUtil.urlDecode(authzRequest.getScope())); + + if (StringUtils.isNotBlank(authzRequest.getDeviceSession())) { + authzRequest.setDeviceSessionObject(deviceSessionService.getDeviceSession(authzRequest.getDeviceSession())); + } + } + + public Response authorize(AuthzRequest authzRequest) throws IOException, TokenBindingParseException { + final String state = authzRequest.getState(); + final String tokenBindingHeader = authzRequest.getHttpRequest().getHeader("Sec-Token-Binding"); + + prepareAuthzRequest(authzRequest); + + SessionId sessionUser = identity.getSessionId(); + User user = sessionIdService.getUser(sessionUser); + + final Client client = authorizeRestWebServiceValidator.validateClient(authzRequest, false); + authorizationChallengeValidator.validateGrantType(client, state); + Set scopes = scopeChecker.checkScopesPolicy(client, authzRequest.getScope()); + + final ExecutionContext executionContext = ExecutionContext.of(authzRequest); + + if (user == null) { + log.trace("Executing external authentication challenge"); + + final boolean ok = externalAuthorizationChallengeService.externalAuthorize(executionContext); + if (!ok) { + log.debug("Not allowed by authorization challenge script, client_id {}.", client.getClientId()); + throw new WebApplicationException(errorResponseFactory + .newErrorResponse(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.ACCESS_DENIED, state, "No allowed by authorization challenge script.")) + .build()); + } + + user = executionContext.getUser() != null ? executionContext.getUser() : new User(); + + // generate session if not exist and if allowed by config + if (sessionUser == null) { + sessionUser = generateAuthenticateSessionWithCookie(authzRequest, user); + } + } + + String grantAcr = executionContext.getScript() != null ? executionContext.getScript().getName() : authzRequest.getAcrValues(); + + AuthorizationCodeGrant authorizationGrant = authorizationGrantList.createAuthorizationCodeGrant(user, client, new Date()); + authorizationGrant.setNonce(authzRequest.getNonce()); + authorizationGrant.setJwtAuthorizationRequest(authzRequest.getJwtRequest()); + authorizationGrant.setTokenBindingHash(TokenBindingMessage.getTokenBindingIdHashFromTokenBindingMessage(tokenBindingHeader, client.getIdTokenTokenBindingCnf())); + authorizationGrant.setScopes(scopes); + authorizationGrant.setCodeChallenge(authzRequest.getCodeChallenge()); + authorizationGrant.setCodeChallengeMethod(authzRequest.getCodeChallengeMethod()); + authorizationGrant.setClaims(authzRequest.getClaims()); + authorizationGrant.setSessionDn(sessionUser != null ? sessionUser.getDn() : "no_session_for_authorization_challenge"); // no need for session as at Authorization Endpoint + authorizationGrant.setAcrValues(grantAcr); + authorizationGrant.save(); + + String authorizationCode = authorizationGrant.getAuthorizationCode().getCode(); + + return createSuccessfulResponse(authorizationCode); + } + + private SessionId generateAuthenticateSessionWithCookie(AuthzRequest authzRequest, User user) { + if (user == null) { + log.trace("Skip session_id generation because user is null"); + return null; + } + if (isFalse(appConfiguration.getAuthorizationChallengeShouldGenerateSession())) { + log.trace("Skip session_id generation because it's not allowed by AS configuration ('authorizationChallengeShouldGenerateSession=false')"); + return null; + } + + Map genericRequestMap = getGenericRequestMap(authzRequest.getHttpRequest()); + + Map parameterMap = Maps.newHashMap(genericRequestMap); + Map requestParameterMap = requestParameterService.getAllowedParameters(parameterMap); + + SessionId sessionUser = sessionIdService.generateAuthenticatedSessionId(authzRequest.getHttpRequest(), user.getDn(), authzRequest.getPrompt()); + sessionUser.setSessionAttributes(requestParameterMap); + + cookieService.createSessionIdCookie(sessionUser, authzRequest.getHttpRequest(), authzRequest.getHttpResponse(), false); + sessionIdService.updateSessionId(sessionUser); + + return sessionUser; + } + + public Response createSuccessfulResponse(String authorizationCode) throws IOException { + AuthorizationChallengeResponse response = new AuthorizationChallengeResponse(); + response.setAuthorizationCode(authorizationCode); + + return Response.status(Response.Status.OK) + .entity(ServerUtil.asJson(response)) + .cacheControl(ServerUtil.cacheControl(true)) + .type(MediaType.APPLICATION_JSON_TYPE).build(); + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeValidator.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeValidator.java new file mode 100644 index 00000000000..b7bcb308e0b --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeValidator.java @@ -0,0 +1,63 @@ +package io.jans.as.server.authorize.ws.rs; + +import io.jans.as.common.model.registration.Client; +import io.jans.as.model.authorize.AuthorizeErrorResponseType; +import io.jans.as.model.common.GrantType; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.error.ErrorResponseFactory; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import org.slf4j.Logger; + +import javax.inject.Named; +import java.util.Arrays; +import java.util.Set; + +/** + * @author Yuriy Z + */ +@RequestScoped +@Named +public class AuthorizationChallengeValidator { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + public void validateGrantType(Client client, String state) { + if (client == null) { + final String msg = "Unable to find client."; + log.debug(msg); + throw new WebApplicationException(errorResponseFactory + .newErrorResponse(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.UNAUTHORIZED_CLIENT, state, msg)) + .build()); + } + + if (client.getGrantTypes() == null || !Arrays.asList(client.getGrantTypes()).contains(GrantType.AUTHORIZATION_CODE)) { + String msg = String.format("Client %s does not support grant_type=authorization_code", client.getClientId()); + log.debug(msg); + throw new WebApplicationException(errorResponseFactory + .newErrorResponse(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.UNAUTHORIZED_CLIENT, state, msg)) + .build()); + } + + final Set grantTypesSupported = appConfiguration.getGrantTypesSupported(); + if (grantTypesSupported == null || !grantTypesSupported.contains(GrantType.AUTHORIZATION_CODE)) { + String msg = "AS configuration does not allow grant_type=authorization_code"; + log.debug(msg); + throw new WebApplicationException(errorResponseFactory + .newErrorResponse(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.UNAUTHORIZED_CLIENT, state, msg)) + .build()); + } + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java index 5e0c3fce994..4a494b8124c 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java @@ -73,6 +73,7 @@ import java.util.function.Function; import static io.jans.as.model.util.StringUtils.implode; +import static io.jans.as.server.authorize.ws.rs.AuthzRequestService.canLogWebApplicationException; import static org.apache.commons.lang3.BooleanUtils.isTrue; import static org.apache.commons.lang3.BooleanUtils.toBoolean; @@ -174,8 +175,6 @@ public Response requestAuthorizationGet( String codeChallenge, String codeChallengeMethod, String customResponseHeaders, String claims, String authReqId, HttpServletRequest httpRequest, HttpServletResponse httpResponse, SecurityContext securityContext) { - authorizeRestWebServiceValidator.validateNotWebView(httpRequest); - AuthzRequest authzRequest = new AuthzRequest(); authzRequest.setHttpMethod(HttpMethod.GET); authzRequest.setScope(scope); @@ -218,8 +217,6 @@ public Response requestAuthorizationPost( String codeChallenge, String codeChallengeMethod, String customResponseHeaders, String claims, String authReqId, HttpServletRequest httpRequest, HttpServletResponse httpResponse, SecurityContext securityContext) { - authorizeRestWebServiceValidator.validateNotWebView(httpRequest); - AuthzRequest authzRequest = new AuthzRequest(); authzRequest.setHttpMethod(HttpMethod.POST); authzRequest.setScope(scope); @@ -254,6 +251,8 @@ public Response requestAuthorizationPost( } private Response requestAuthorization(AuthzRequest authzRequest) { + authorizeRestWebServiceValidator.validateNotWebView(authzRequest.getHttpRequest()); + authzRequest.setScope(ServerUtil.urlDecode(authzRequest.getScope())); // it may be encoded -> decode authzRequestService.createOauth2AuditLog(authzRequest); @@ -302,14 +301,6 @@ private Response requestAuthorization(AuthzRequest authzRequest) { return builder.build(); } - private static boolean canLogWebApplicationException(WebApplicationException e) { - if (e == null || e.getResponse() == null) { - return false; - } - final int status = e.getResponse().getStatus(); - return status != 302; - } - private ResponseBuilder authorize(AuthzRequest authzRequest) throws AcrChangedException, SearchException, TokenBindingParseException { String tokenBindingHeader = authzRequest.getHttpRequest().getHeader("Sec-Token-Binding"); boolean isPar = authzRequestService.processPar(authzRequest); @@ -857,7 +848,7 @@ private void updateSessionForROPC(HttpServletRequest httpRequest, SessionId sess } } - private Map getGenericRequestMap(HttpServletRequest httpRequest) { + public static Map getGenericRequestMap(HttpServletRequest httpRequest) { Map result = new HashMap<>(); for (Entry entry : httpRequest.getParameterMap().entrySet()) { result.put(entry.getKey(), entry.getValue()[0]); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceValidator.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceValidator.java index e2753e8f128..7f44f4403d2 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceValidator.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceValidator.java @@ -97,6 +97,7 @@ public Client validateClient(AuthzRequest authzRequest, boolean isPar) { public Client validateClient(String clientId, String state, boolean isPar) { if (StringUtils.isBlank(clientId)) { + log.debug("client_id is empty or blank {}.", clientId); throw new WebApplicationException(Response .status(Response.Status.BAD_REQUEST) .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.UNAUTHORIZED_CLIENT, state, "client_id is empty or blank.")) @@ -107,6 +108,7 @@ public Client validateClient(String clientId, String state, boolean isPar) { try { final Client client = clientService.getClient(clientId); if (client == null) { + log.debug("Unable to find client by id {}.", clientId); throw new WebApplicationException(Response .status(Response.Status.UNAUTHORIZED) .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.UNAUTHORIZED_CLIENT, state, "Unable to find client.")) @@ -114,6 +116,7 @@ public Client validateClient(String clientId, String state, boolean isPar) { .build()); } if (client.isDisabled()) { + log.debug("Client {} is disabled.", clientId); throw new WebApplicationException(Response .status(Response.Status.UNAUTHORIZED) .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.DISABLED_CLIENT, state, "Client is disabled.")) @@ -132,6 +135,7 @@ public Client validateClient(String clientId, String state, boolean isPar) { return client; } catch (EntryPersistenceException e) { // Invalid clientId + log.debug("Unable to find client on AS by client_id: {}", clientId); throw new WebApplicationException(Response .status(Response.Status.UNAUTHORIZED) .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.UNAUTHORIZED_CLIENT, state, "Unable to find client on AS.")) diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequest.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequest.java index 1b6a8288ed1..2433d6f0ce1 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequest.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequest.java @@ -1,6 +1,7 @@ package io.jans.as.server.authorize.ws.rs; import io.jans.as.common.model.registration.Client; +import io.jans.as.common.model.session.DeviceSession; import io.jans.as.model.common.Prompt; import io.jans.as.model.common.ResponseType; import io.jans.as.server.model.audit.OAuth2AuditLog; @@ -46,6 +47,9 @@ public class AuthzRequest { private String claims; private String authReqId; private String httpMethod; + private String deviceSession; + private DeviceSession deviceSessionObject; + private boolean useDeviceSession; private HttpServletRequest httpRequest; private HttpServletResponse httpResponse; private SecurityContext securityContext; @@ -56,6 +60,30 @@ public class AuthzRequest { private OAuth2AuditLog auditLog; private boolean promptFromJwt; + public DeviceSession getDeviceSessionObject() { + return deviceSessionObject; + } + + public void setDeviceSessionObject(DeviceSession deviceSessionObject) { + this.deviceSessionObject = deviceSessionObject; + } + + public boolean isUseDeviceSession() { + return useDeviceSession; + } + + public void setUseDeviceSession(boolean useDeviceSession) { + this.useDeviceSession = useDeviceSession; + } + + public String getDeviceSession() { + return deviceSession; + } + + public void setDeviceSession(String deviceSession) { + this.deviceSession = deviceSession; + } + public boolean isPromptFromJwt() { return promptFromJwt; } @@ -368,6 +396,7 @@ public String toString() { ", idTokenHint='" + idTokenHint + '\'' + ", loginHint='" + loginHint + '\'' + ", acrValues='" + acrValues + '\'' + + ", deviceSession='" + deviceSession + '\'' + ", amrValues='" + amrValues + '\'' + ", request='" + request + '\'' + ", requestUri='" + requestUri + '\'' + @@ -376,6 +405,7 @@ public String toString() { ", codeChallenge='" + codeChallenge + '\'' + ", codeChallengeMethod='" + codeChallengeMethod + '\'' + ", customResponseHeaders='" + customResponseHeaders + '\'' + + ", customParameters='" + customParameters+ '\'' + ", claims='" + claims + '\'' + ", authReqId='" + authReqId + '\'' + ", httpRequest=" + httpRequest + diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java index 1f10be93773..e21a9e19397 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java @@ -555,8 +555,20 @@ public void createRedirectUriResponse(AuthzRequest authzRequest) { authzRequest.setRedirectUriResponse(redirectUriResponse); } + public static boolean canLogWebApplicationException(WebApplicationException e) { + if (e == null || e.getResponse() == null) { + return false; + } + final int status = e.getResponse().getStatus(); + return status != 302; + } + public void createOauth2AuditLog(AuthzRequest authzRequest) { - OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(authzRequest.getHttpRequest()), Action.USER_AUTHORIZATION); + createOauth2AuditLog(authzRequest, Action.USER_AUTHORIZATION); + } + + public void createOauth2AuditLog(AuthzRequest authzRequest, Action action) { + OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(authzRequest.getHttpRequest()), action); oAuth2AuditLog.setClientId(authzRequest.getClientId()); oAuth2AuditLog.setScope(authzRequest.getScope()); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/DeviceSessionService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/DeviceSessionService.java new file mode 100644 index 00000000000..9d47dd015a9 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/DeviceSessionService.java @@ -0,0 +1,62 @@ +package io.jans.as.server.authorize.ws.rs; + +import io.jans.as.common.model.session.DeviceSession; +import io.jans.as.model.config.StaticConfiguration; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.orm.PersistenceEntryManager; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; + +/** + * @author Yuriy Z + */ +@Named +@ApplicationScoped +public class DeviceSessionService { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private PersistenceEntryManager persistenceEntryManager; + + @Inject + private StaticConfiguration staticConfiguration; + + public String buildDn(String id) { + return String.format("jansId=%s,%s", id, staticConfiguration.getBaseDn().getSessions()); + } + + public DeviceSession getDeviceSessionByDn(String dn) { + try { + return persistenceEntryManager.find(DeviceSession.class, dn); + } catch (Exception e) { + log.trace(e.getMessage(), e); + return null; + } + } + + public DeviceSession getDeviceSession(String id) { + if (StringUtils.isNotBlank(id)) { + DeviceSession result = getDeviceSessionByDn(buildDn(id)); + log.debug("Found {} entries for deviceSession id = {}", result != null ? 1 : 0, id); + + return result; + } + return null; + } + + public void persist(DeviceSession entity) { + persistenceEntryManager.persist(entity); + } + + public void merge(DeviceSession entity) { + persistenceEntryManager.merge(entity); + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/audit/Action.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/audit/Action.java index 17511badb5d..5dc7749f020 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/audit/Action.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/audit/Action.java @@ -15,6 +15,7 @@ public enum Action { CLIENT_READ("CLIENT_READ"), CLIENT_DELETE("CLIENT_DELETE"), USER_AUTHORIZATION("USER_AUTHORIZATION"), + AUTHORIZATION_CHALLENGE("AUTHORIZATION_CHALLENGE"), BACKCHANNEL_AUTHENTICATION("BACKCHANNEL_AUTHENTICATION"), BACKCHANNEL_DEVICE_REGISTRATION("BACKCHANNEL_DEVICE_REGISTRATION"), USER_INFO("USER_INFO"), diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/ResteasyInitializer.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/ResteasyInitializer.java index 053d8a1e32b..606dbb45eca 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/ResteasyInitializer.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/ResteasyInitializer.java @@ -6,6 +6,7 @@ package io.jans.as.server.service; +import io.jans.as.server.authorize.ws.rs.AuthorizationChallengeEndpoint; import io.jans.as.server.authorize.ws.rs.AuthorizeRestWebServiceImpl; import io.jans.as.server.authorize.ws.rs.DeviceAuthorizationRestWebServiceImpl; import io.jans.as.server.bcauthorize.ws.rs.BackchannelAuthorizeRestWebServiceImpl; @@ -23,18 +24,12 @@ import io.jans.as.server.session.ws.rs.SessionRestWebService; import io.jans.as.server.ssa.ws.rs.SsaRestWebServiceImpl; import io.jans.as.server.token.ws.rs.TokenRestWebServiceImpl; -import io.jans.as.server.uma.ws.rs.UmaGatheringWS; -import io.jans.as.server.uma.ws.rs.UmaMetadataWS; -import io.jans.as.server.uma.ws.rs.UmaPermissionRegistrationWS; -import io.jans.as.server.uma.ws.rs.UmaResourceRegistrationWS; -import io.jans.as.server.uma.ws.rs.UmaRptIntrospectionWS; -import io.jans.as.server.uma.ws.rs.UmaScopeIconWS; -import io.jans.as.server.uma.ws.rs.UmaScopeWS; +import io.jans.as.server.uma.ws.rs.*; import io.jans.as.server.userinfo.ws.rs.UserInfoRestWebServiceImpl; import io.jans.as.server.ws.rs.stat.StatWS; - import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; + import java.util.HashSet; import java.util.Set; @@ -53,6 +48,7 @@ public Set> getClasses() { classes.add(JansConfigurationWS.class); classes.add(AuthorizeRestWebServiceImpl.class); + classes.add(AuthorizationChallengeEndpoint.class); classes.add(RegisterRestWebServiceImpl.class); classes.add(ClientInfoRestWebServiceImpl.class); classes.add(RevokeRestWebServiceImpl.class); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthorizationChallengeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthorizationChallengeService.java new file mode 100644 index 00000000000..41f8bcb0d68 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthorizationChallengeService.java @@ -0,0 +1,95 @@ +package io.jans.as.server.service.external; + +import io.jans.as.model.authorize.AuthorizeErrorResponseType; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.server.model.common.ExecutionContext; +import io.jans.as.server.service.external.context.ExternalScriptContext; +import io.jans.model.custom.script.CustomScriptType; +import io.jans.model.custom.script.conf.CustomScriptConfiguration; +import io.jans.model.custom.script.type.authzchallenge.AuthorizationChallengeType; +import io.jans.service.custom.script.ExternalScriptService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; + +import java.util.List; + +/** + * Authorization Challenge service responsible for external script interaction. + * + * @author Yuriy Z + */ +@ApplicationScoped +public class ExternalAuthorizationChallengeService extends ExternalScriptService { + + @Inject + private transient AppConfiguration appConfiguration; + + @Inject + private transient ErrorResponseFactory errorResponseFactory; + + public ExternalAuthorizationChallengeService() { + super(CustomScriptType.AUTHORIZATION_CHALLENGE); + } + + public boolean externalAuthorize(ExecutionContext executionContext) { + final List acrValues = executionContext.getAuthzRequest().getAcrValuesList(); + final CustomScriptConfiguration script = identifyScript(acrValues); + if (script == null) { + String msg = String.format("Unable to identify script by acr_values %s.", acrValues); + log.debug(msg); + throw new WebApplicationException(errorResponseFactory + .newErrorResponse(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.INVALID_REQUEST, executionContext.getAuthzRequest().getState(), msg)) + .build()); + } + + log.trace("Executing python 'authorize' method, script name: {}, clientId: {}, scope: {}, deviceSession: {}", + script.getName(), executionContext.getAuthzRequest().getClientId(), executionContext.getAuthzRequest().getScope(), executionContext.getAuthzRequest().getDeviceSession()); + + executionContext.setScript(script); + + boolean result = false; + try { + AuthorizationChallengeType authorizationChallengeType = (AuthorizationChallengeType) script.getExternalType(); + final ExternalScriptContext scriptContext = new ExternalScriptContext(executionContext); + result = authorizationChallengeType.authorize(scriptContext); + + scriptContext.throwWebApplicationExceptionIfSet(); + } catch (WebApplicationException e) { + if (log.isTraceEnabled()) { + log.trace("WebApplicationException from script", e); + } + throw e; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + } + + log.trace("Finished 'authorize' method, script name: {}, clientId: {}, result: {}", script.getName(), executionContext.getAuthzRequest().getClientId(), result); + + return result; + } + + public CustomScriptConfiguration identifyScript(List acrValues) { + log.trace("Identifying script, acr_values: {}", acrValues); + + if (acrValues == null || acrValues.isEmpty()) { + log.trace("No acr_values, return default script"); + return getCustomScriptConfigurationByName(appConfiguration.getAuthorizationChallengeDefaultAcr()); + } + + for (String acr : acrValues) { + final CustomScriptConfiguration script = getCustomScriptConfigurationByName(acr); + if (script != null) { + log.trace("Found script {} by acr {}", script.getInum(), acr); + return script; + } + } + + log.trace("Unable to find script by acr_values {}", acrValues); + return getCustomScriptConfigurationByName(appConfiguration.getAuthorizationChallengeDefaultAcr()); + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/DynamicScopeExternalContext.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/DynamicScopeExternalContext.java index 22447f0c287..0424761c1b0 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/DynamicScopeExternalContext.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/DynamicScopeExternalContext.java @@ -10,6 +10,7 @@ import io.jans.as.model.token.JsonWebResponse; import io.jans.as.persistence.model.Scope; import io.jans.as.server.model.common.IAuthorizationGrant; +import jakarta.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; @@ -27,7 +28,7 @@ public class DynamicScopeExternalContext extends ExternalScriptContext { private final IAuthorizationGrant authorizationGrant; public DynamicScopeExternalContext(List dynamicScopes, JsonWebResponse jsonWebResponse, IAuthorizationGrant authorizationGrant) { - super(null); + super((HttpServletRequest) null); this.dynamicScopes = dynamicScopes; this.jsonWebResponse = jsonWebResponse; diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java index 207f3953c7f..db30f42535d 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java @@ -7,12 +7,15 @@ package io.jans.as.server.service.external.context; import io.jans.as.model.util.Util; +import io.jans.as.server.authorize.ws.rs.AuthzRequest; +import io.jans.as.server.model.common.ExecutionContext; import io.jans.as.server.util.ServerUtil; import io.jans.orm.PersistenceEntryManager; import io.jans.orm.exception.EntryPersistenceException; import io.jans.orm.model.base.CustomEntry; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.core.CacheControl; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.apache.commons.net.util.SubnetUtils; @@ -30,21 +33,36 @@ public class ExternalScriptContext extends io.jans.service.external.context.Exte private static final Logger log = LoggerFactory.getLogger(ExternalScriptContext.class); - private final PersistenceEntryManager ldapEntryManager; + private final PersistenceEntryManager persistenceEntryManager; + + private ExecutionContext executionContext; private NoLogWebApplicationException webApplicationException; + public ExternalScriptContext(ExecutionContext executionContext) { + this(executionContext.getHttpRequest(), executionContext.getHttpResponse()); + this.executionContext = executionContext; + } + public ExternalScriptContext(HttpServletRequest httpRequest) { this(httpRequest, null); } public ExternalScriptContext(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { super(httpRequest, httpResponse); - this.ldapEntryManager = ServerUtil.getLdapManager(); + this.persistenceEntryManager = ServerUtil.getLdapManager(); + } + + public ExecutionContext getExecutionContext() { + return executionContext; + } + + public AuthzRequest getAuthzRequest() { + return executionContext != null ? executionContext.getAuthzRequest() : null; } public PersistenceEntryManager getPersistenceEntryManager() { - return ldapEntryManager; + return persistenceEntryManager; } public boolean isInNetwork(String cidrNotation) { @@ -58,7 +76,7 @@ public boolean isInNetwork(String cidrNotation) { protected CustomEntry getEntryByDn(String dn, String... ldapReturnAttributes) { try { - return ldapEntryManager.find(dn, CustomEntry.class, ldapReturnAttributes); + return persistenceEntryManager.find(dn, CustomEntry.class, ldapReturnAttributes); } catch (EntryPersistenceException epe) { log.error("Failed to find entry '{}'", dn); } @@ -88,10 +106,14 @@ public NoLogWebApplicationException createWebApplicationException(Response respo } public NoLogWebApplicationException createWebApplicationException(int status, String entity) { + final CacheControl cacheControl = new CacheControl(); + cacheControl.setNoStore(true); + this.webApplicationException = new NoLogWebApplicationException(Response .status(status) .entity(entity) .type(MediaType.APPLICATION_JSON_TYPE) + .cacheControl(cacheControl) .build()); return this.webApplicationException; } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/FapiOpenIdConfiguration.java b/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/FapiOpenIdConfiguration.java index bd75a12c4ad..3063f6d1c1b 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/FapiOpenIdConfiguration.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/FapiOpenIdConfiguration.java @@ -200,6 +200,7 @@ protected void processRequest(HttpServletRequest servletRequest, HttpServletResp jsonObj.put(ISSUER, appConfiguration.getIssuer()); jsonObj.put(AUTHORIZATION_ENDPOINT, appConfiguration.getAuthorizationEndpoint()); + jsonObj.put(AUTHORIZATION_CHALLENGE_ENDPOINT, appConfiguration.getAuthorizationChallengeEndpoint()); jsonObj.put(TOKEN_ENDPOINT, appConfiguration.getTokenEndpoint()); jsonObj.put(REVOCATION_ENDPOINT, appConfiguration.getTokenRevocationEndpoint()); jsonObj.put(SESSION_REVOCATION_ENDPOINT, endpointUrl("/revoke_session")); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/OpenIdConfiguration.java b/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/OpenIdConfiguration.java index 954e16e0737..d67ba838d44 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/OpenIdConfiguration.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/OpenIdConfiguration.java @@ -106,6 +106,7 @@ protected void processRequest(HttpServletRequest servletRequest, HttpServletResp jsonObj.put(ISSUER, appConfiguration.getIssuer()); jsonObj.put(AUTHORIZATION_ENDPOINT, appConfiguration.getAuthorizationEndpoint()); + jsonObj.put(AUTHORIZATION_CHALLENGE_ENDPOINT, appConfiguration.getAuthorizationChallengeEndpoint()); jsonObj.put(TOKEN_ENDPOINT, appConfiguration.getTokenEndpoint()); jsonObj.put(JWKS_URI, appConfiguration.getJwksUri()); jsonObj.put(CHECK_SESSION_IFRAME, appConfiguration.getCheckSessionIFrame()); @@ -268,6 +269,8 @@ private void addMtlsAliases(JSONObject jsonObj) { if (StringUtils.isNotBlank(appConfiguration.getMtlsAuthorizationEndpoint())) aliases.put(AUTHORIZATION_ENDPOINT, appConfiguration.getMtlsAuthorizationEndpoint()); + if (StringUtils.isNotBlank(appConfiguration.getMtlsAuthorizationChallengeEndpoint())) + aliases.put(AUTHORIZATION_CHALLENGE_ENDPOINT, appConfiguration.getMtlsAuthorizationChallengeEndpoint()); if (StringUtils.isNotBlank(appConfiguration.getMtlsTokenEndpoint())) aliases.put(TOKEN_ENDPOINT, appConfiguration.getMtlsTokenEndpoint()); if (StringUtils.isNotBlank(appConfiguration.getMtlsJwksUri())) diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/uma/ws/rs/UmaMetadataWS.java b/jans-auth-server/server/src/main/java/io/jans/as/server/uma/ws/rs/UmaMetadataWS.java index 9264bfaa7f8..5f98f37120c 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/uma/ws/rs/UmaMetadataWS.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/uma/ws/rs/UmaMetadataWS.java @@ -73,6 +73,7 @@ public Response getConfiguration() { c.setRegistrationEndpoint(appConfiguration.getRegistrationEndpoint()); c.setTokenEndpoint(appConfiguration.getTokenEndpoint()); c.setAuthorizationEndpoint(appConfiguration.getAuthorizationEndpoint()); + c.setAuthorizationChallengeEndpoint(appConfiguration.getAuthorizationChallengeEndpoint()); c.setIntrospectionEndpoint(baseEndpointUri + "/rpt/status"); c.setResourceRegistrationEndpoint(baseEndpointUri + "/host/rsrc/resource_set"); c.setPermissionEndpoint(baseEndpointUri + "/host/rsrc_pr"); diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeValidatorTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeValidatorTest.java new file mode 100644 index 00000000000..036706a24a1 --- /dev/null +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeValidatorTest.java @@ -0,0 +1,81 @@ +package io.jans.as.server.authorize.ws.rs; + +import com.google.common.collect.Sets; +import io.jans.as.common.model.registration.Client; +import io.jans.as.model.common.GrantType; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.error.ErrorResponseFactory; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.slf4j.Logger; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import static org.mockito.Mockito.when; + +/** + * @author Yuriy Z + */ +@Listeners(MockitoTestNGListener.class) +public class AuthorizationChallengeValidatorTest { + + @InjectMocks + private AuthorizationChallengeValidator authorizationChallengeValidator; + + @Mock + private Logger log; + + @Mock + private AppConfiguration appConfiguration; + + @Mock + private ErrorResponseFactory errorResponseFactory; + + @Test(expectedExceptions = WebApplicationException.class) + public void validateGrantType_whenClientIsNull_shouldThrowError() { + when(errorResponseFactory.newErrorResponse(Response.Status.BAD_REQUEST)).thenCallRealMethod(); + + authorizationChallengeValidator.validateGrantType(null, null); + } + + @Test(expectedExceptions = WebApplicationException.class) + public void validateGrantType_whenClientGrantTypesAreNull_shouldThrowError() { + when(errorResponseFactory.newErrorResponse(Response.Status.BAD_REQUEST)).thenCallRealMethod(); + + authorizationChallengeValidator.validateGrantType(new Client(), null); + } + + @Test(expectedExceptions = WebApplicationException.class) + public void validateGrantType_whenClientGrantTypesDoesNotHaveAuthorizationCode_shouldThrowError() { + when(errorResponseFactory.newErrorResponse(Response.Status.BAD_REQUEST)).thenCallRealMethod(); + + final Client client = new Client(); + client.setGrantTypes(new GrantType[]{GrantType.CLIENT_CREDENTIALS}); + + authorizationChallengeValidator.validateGrantType(client, null); + } + + @Test(expectedExceptions = WebApplicationException.class) + public void validateGrantType_whenGrantTypeIsNotAllowedByConfig_shouldThrowError() { + when(errorResponseFactory.newErrorResponse(Response.Status.BAD_REQUEST)).thenCallRealMethod(); + when(appConfiguration.getGrantTypesSupported()).thenReturn(Sets.newHashSet(GrantType.IMPLICIT)); + + final Client client = new Client(); + client.setGrantTypes(new GrantType[]{GrantType.AUTHORIZATION_CODE}); + + authorizationChallengeValidator.validateGrantType(client, null); + } + + @Test + public void validateGrantType_whenGrantTypeIsAllowedByConfigAndClient_shouldPassSuccessfully() { + when(appConfiguration.getGrantTypesSupported()).thenReturn(Sets.newHashSet(GrantType.IMPLICIT, GrantType.AUTHORIZATION_CODE)); + + final Client client = new Client(); + client.setGrantTypes(new GrantType[]{GrantType.AUTHORIZATION_CODE}); + + authorizationChallengeValidator.validateGrantType(client, "state"); + } +} diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/service/TestResteasyInitializer.java b/jans-auth-server/server/src/test/java/io/jans/as/server/service/TestResteasyInitializer.java index 836481f5932..cb6dcc2c6dd 100644 --- a/jans-auth-server/server/src/test/java/io/jans/as/server/service/TestResteasyInitializer.java +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/service/TestResteasyInitializer.java @@ -6,6 +6,7 @@ package io.jans.as.server.service; +import io.jans.as.server.authorize.ws.rs.AuthorizationChallengeEndpoint; import io.jans.as.server.authorize.ws.rs.AuthorizeRestWebServiceImpl; import io.jans.as.server.clientinfo.ws.rs.ClientInfoRestWebServiceImpl; import io.jans.as.server.introspection.ws.rs.IntrospectionWebService; @@ -14,27 +15,15 @@ import io.jans.as.server.register.ws.rs.RegisterRestWebServiceImpl; import io.jans.as.server.session.ws.rs.EndSessionRestWebServiceImpl; import io.jans.as.server.token.ws.rs.TokenRestWebServiceImpl; -import io.jans.as.server.uma.ws.rs.UmaGatheringWS; -import io.jans.as.server.uma.ws.rs.UmaMetadataWS; -import io.jans.as.server.uma.ws.rs.UmaPermissionRegistrationWS; -import io.jans.as.server.uma.ws.rs.UmaResourceRegistrationWS; -import io.jans.as.server.uma.ws.rs.UmaRptIntrospectionWS; -import io.jans.as.server.uma.ws.rs.UmaScopeWS; +import io.jans.as.server.uma.ws.rs.*; import io.jans.as.server.userinfo.ws.rs.UserInfoRestWebServiceImpl; - import io.jans.as.server.util.TestUtil; -import jakarta.servlet.Servlet; -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletException; import jakarta.ws.rs.core.Application; import jakarta.ws.rs.ext.Provider; -import java.util.Enumeration; import java.util.HashSet; import java.util.Set; -import static org.omnifaces.util.Faces.getServletContext; - /** * Integration with Resteasy * @@ -52,6 +41,7 @@ public Set> getClasses() { return classes; } classes.add(AuthorizeRestWebServiceImpl.class); + classes.add(AuthorizationChallengeEndpoint.class); classes.add(TokenRestWebServiceImpl.class); classes.add(RegisterRestWebServiceImpl.class); classes.add(UserInfoRestWebServiceImpl.class); diff --git a/jans-auth-server/server/src/test/resources/testng.xml b/jans-auth-server/server/src/test/resources/testng.xml index 88b3444348b..7a224c4a497 100644 --- a/jans-auth-server/server/src/test/resources/testng.xml +++ b/jans-auth-server/server/src/test/resources/testng.xml @@ -30,6 +30,7 @@ + diff --git a/jans-config-api/server/src/test/resources/feature/config/properties/endpoints/endpoints.json b/jans-config-api/server/src/test/resources/feature/config/properties/endpoints/endpoints.json index 4a9ed29a846..0c82412f980 100644 --- a/jans-config-api/server/src/test/resources/feature/config/properties/endpoints/endpoints.json +++ b/jans-config-api/server/src/test/resources/feature/config/properties/endpoints/endpoints.json @@ -4,6 +4,7 @@ "umaConfigurationEndpoint": "https://pujavs3.infinity.com/jans-auth/restv1/uma2-configuration", "clientInfoEndpoint": "https://pujavs3.infinity.com/jans-auth/restv1/clientinfo", "authorizationEndpoint": "https://pujavs3.infinity.com/jans-auth/restv1/authorize", + "authorizationChallengeEndpoint":"https://pujavs3.infinity.com/jans-auth/restv1/authorization_challenge", "backchannelAuthenticationEndpoint": "https://pujavs3.infinity.com/jans-auth/restv1/bc-authorize", "baseEndpoint": "https://pujavs3.infinity.com/jans-auth/restv1", "tokenEndpoint": "https://pujavs3.infinity.com/jans-auth/restv1/token", diff --git a/jans-core/script/src/main/java/io/jans/model/custom/script/CustomScriptType.java b/jans-core/script/src/main/java/io/jans/model/custom/script/CustomScriptType.java index 3e48d86f8c1..f7c41b2aea9 100644 --- a/jans-core/script/src/main/java/io/jans/model/custom/script/CustomScriptType.java +++ b/jans-core/script/src/main/java/io/jans/model/custom/script/CustomScriptType.java @@ -13,6 +13,8 @@ import io.jans.model.custom.script.type.auth.PersonAuthenticationType; import io.jans.model.custom.script.type.authz.ConsentGatheringType; import io.jans.model.custom.script.type.authz.DummyConsentGatheringType; +import io.jans.model.custom.script.type.authzchallenge.AuthorizationChallengeType; +import io.jans.model.custom.script.type.authzchallenge.DummyAuthorizationChallengeType; import io.jans.model.custom.script.type.ciba.DummyEndUserNotificationType; import io.jans.model.custom.script.type.ciba.EndUserNotificationType; import io.jans.model.custom.script.type.client.ClientRegistrationType; @@ -70,6 +72,7 @@ public enum CustomScriptType implements AttributeEnum { PERSON_AUTHENTICATION("person_authentication", "Person Authentication", PersonAuthenticationType.class, AuthenticationCustomScript.class, "PersonAuthentication", new DummyPersonAuthenticationType()), + AUTHORIZATION_CHALLENGE("authorization_challenge", "Authorization Challenge", AuthorizationChallengeType.class, CustomScript.class, "AuthorizationChallenge", new DummyAuthorizationChallengeType()), INTROSPECTION("introspection", "Introspection", IntrospectionType.class, CustomScript.class, "Introspection", new DummyIntrospectionType()), RESOURCE_OWNER_PASSWORD_CREDENTIALS("resource_owner_password_credentials", "Resource Owner Password Credentials", ResourceOwnerPasswordCredentialsType.class, CustomScript.class, "ResourceOwnerPasswordCredentials", new DummyResourceOwnerPasswordCredentialsType()), APPLICATION_SESSION("application_session", "Application Session", ApplicationSessionType.class, CustomScript.class, "ApplicationSession", diff --git a/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/AuthorizationChallengeType.java b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/AuthorizationChallengeType.java new file mode 100644 index 00000000000..5db9bc930f3 --- /dev/null +++ b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/AuthorizationChallengeType.java @@ -0,0 +1,11 @@ +package io.jans.model.custom.script.type.authzchallenge; + +import io.jans.model.custom.script.type.BaseExternalType; + +/** + * @author Yuriy Z + */ +public interface AuthorizationChallengeType extends BaseExternalType { + + boolean authorize(Object context); +} diff --git a/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/DummyAuthorizationChallengeType.java b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/DummyAuthorizationChallengeType.java new file mode 100644 index 00000000000..702ba2ad431 --- /dev/null +++ b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/DummyAuthorizationChallengeType.java @@ -0,0 +1,37 @@ +package io.jans.model.custom.script.type.authzchallenge; + +import io.jans.model.SimpleCustomProperty; +import io.jans.model.custom.script.model.CustomScript; + +import java.util.Map; + +/** + * @author Yuriy Z + */ +public class DummyAuthorizationChallengeType implements AuthorizationChallengeType { + + @Override + public boolean authorize(Object context) { + return false; + } + + @Override + public boolean init(Map configurationAttributes) { + return false; + } + + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + return false; + } + + @Override + public boolean destroy(Map configurationAttributes) { + return false; + } + + @Override + public int getApiVersion() { + return 1; + } +} diff --git a/jans-linux-setup/jans_setup/schema/jans_schema.json b/jans-linux-setup/jans_setup/schema/jans_schema.json index 8c41fb777ca..b7c1fc2abc0 100644 --- a/jans-linux-setup/jans_setup/schema/jans_schema.json +++ b/jans-linux-setup/jans_setup/schema/jans_schema.json @@ -3738,6 +3738,29 @@ ], "x_origin": "Jans created objectclass" }, + { + "kind": "STRUCTURAL", + "may": [ + "jansId", + "jansUsrDN", + "creationDate", + "exp", + "del", + "clnId", + "attr" + ], + "must": [ + "objectclass" + ], + "names": [ + "jansDeviceSess" + ], + "oid": "jansObjClass", + "sup": [ + "top" + ], + "x_origin": "Jans created objectclass" + }, { "kind": "STRUCTURAL", "may": [ diff --git a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json index b3a78c9b7d9..e9f8ff0359a 100644 --- a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json +++ b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json @@ -2,6 +2,7 @@ "issuer":"https://%(hostname)s", "baseEndpoint":"https://%(hostname)s/jans-auth/restv1", "authorizationEndpoint":"https://%(hostname)s/jans-auth/restv1/authorize", + "authorizationChallengeEndpoint":"https://%(hostname)s/jans-auth/restv1/authorization_challenge", "tokenEndpoint":"https://%(hostname)s/jans-auth/restv1/token", "tokenRevocationEndpoint": "https://%(hostname)s/jans-auth/restv1/revoke", "userInfoEndpoint":"https://%(hostname)s/jans-auth/restv1/userinfo", diff --git a/jans-linux-setup/jans_setup/templates/scripts.ldif b/jans-linux-setup/jans_setup/templates/scripts.ldif index 63a4eabf9b6..d7dc5623633 100644 --- a/jans-linux-setup/jans_setup/templates/scripts.ldif +++ b/jans-linux-setup/jans_setup/templates/scripts.ldif @@ -535,6 +535,20 @@ jansRevision: 11 jansScr::%(discovery_discovery)s jansScrTyp: discovery +dn: inum=0300-BA99,ou=scripts,o=jans +objectClass: jansCustomScr +objectClass: top +description: Default Authorization Challenge Java Script +displayName: default_challenge +inum: 0300-BA99 +jansEnabled: true +jansLevel: 1 +jansModuleProperty: {"value1":"location_type","value2":"db","description":""} +jansProgLng: java +jansRevision: 11 +jansScr::%(authorization_challenge_authorizationchallenge)s +jansScrTyp: authorization_challenge + dn: inum=BADA-BADA,ou=scripts,o=jans objectClass: jansCustomScr objectClass: top