From fa45075c841713b76260b92f2c1f292f0eeba926 Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Wed, 20 Nov 2024 05:33:49 +0200 Subject: [PATCH] feat(jans-auth-server): openID AuthZEN implementation (#10197) * feat(jans-auth-server): openID AuthZEN implementation * revert: unwanted changes --------- Co-authored-by: moabu <47318409+moabu@users.noreply.github.com> --- docs/assets/log/access-evaluation-run-log.txt | 245 +++++++++++ docs/janssen-server/auth-server/README.md | 1 + .../endpoints/access-evaluation.md | 408 ++++++++++++++++++ .../developer/interception-scripts.md | 1 + .../developer/scripts/README.md | 1 + .../access_evaluation/AccessEvaluation.java | 88 ++++ .../access_evaluation/access-evaluation.md | 172 ++++++++ .../as/client/AccessEvaluationClient.java | 54 +++ .../client/AccessEvaluationClientRequest.java | 63 +++ .../AccessEvaluationClientResponse.java | 52 +++ .../java/io/jans/as/client/BaseClient.java | 14 +- .../java/io/jans/as/client/IsJsonRequest.java | 8 + .../as/client/OpenIdConfigurationClient.java | 1 + .../client/OpenIdConfigurationResponse.java | 22 + .../test/java/io/jans/as/client/BaseTest.java | 16 +- .../ws/rs/AccessEvaluationHttpTest.java | 164 +++++++ .../client/src/test/resources/testng.xml | 6 +- jans-auth-server/docs/swagger.yaml | 95 ++++ .../jans/as/model/common/FeatureFlagType.java | 3 + .../model/configuration/AppConfiguration.java | 27 +- .../ConfigurationResponseClaim.java | 1 + .../AccessEvaluationRestWebServiceImplV1.java | 91 ++++ .../ws/rs/AccessEvaluationService.java | 128 ++++++ .../ws/rs/AccessEvaluationValidator.java | 99 +++++ .../server/model/common/ExecutionContext.java | 15 + .../as/server/service/DiscoveryService.java | 8 + .../server/service/ResteasyInitializer.java | 2 + .../ExternalAccessEvaluationService.java | 80 ++++ ...essEvaluationRestWebServiceImplV1Test.java | 52 +++ .../ws/rs/AccessEvaluationValidatorTest.java | 91 ++++ .../server/src/test/resources/testng.xml | 4 + .../authzen/AccessEvaluationRequest.java | 70 +++ .../authzen/AccessEvaluationResponse.java | 53 +++ .../AccessEvaluationResponseContext.java | 66 +++ .../java/io/jans/model/authzen/Action.java | 46 ++ .../java/io/jans/model/authzen/Context.java | 33 ++ .../java/io/jans/model/authzen/Resource.java | 59 +++ .../java/io/jans/model/authzen/Subject.java | 59 +++ .../model/custom/script/CustomScriptType.java | 4 + .../type/authzen/AccessEvaluationType.java | 13 + .../authzen/DummyAccessEvaluationType.java | 37 ++ .../templates/jans-auth/jans-auth-config.json | 3 +- .../jans_setup/templates/scopes.ldif | 11 + .../jans_setup/templates/scripts.ldif | 14 + .../jans-auth/data/jans-auth-test-data.ldif | 5 + mkdocs.yml | 1 + 46 files changed, 2473 insertions(+), 13 deletions(-) create mode 100644 docs/assets/log/access-evaluation-run-log.txt create mode 100644 docs/janssen-server/auth-server/endpoints/access-evaluation.md create mode 100644 docs/script-catalog/access_evaluation/AccessEvaluation.java create mode 100644 docs/script-catalog/access_evaluation/access-evaluation.md create mode 100644 jans-auth-server/client/src/main/java/io/jans/as/client/AccessEvaluationClient.java create mode 100644 jans-auth-server/client/src/main/java/io/jans/as/client/AccessEvaluationClientRequest.java create mode 100644 jans-auth-server/client/src/main/java/io/jans/as/client/AccessEvaluationClientResponse.java create mode 100644 jans-auth-server/client/src/main/java/io/jans/as/client/IsJsonRequest.java create mode 100644 jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/AccessEvaluationHttpTest.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationRestWebServiceImplV1.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationService.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationValidator.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAccessEvaluationService.java create mode 100644 jans-auth-server/server/src/test/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationRestWebServiceImplV1Test.java create mode 100644 jans-auth-server/server/src/test/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationValidatorTest.java create mode 100644 jans-core/model/src/main/java/io/jans/model/authzen/AccessEvaluationRequest.java create mode 100644 jans-core/model/src/main/java/io/jans/model/authzen/AccessEvaluationResponse.java create mode 100644 jans-core/model/src/main/java/io/jans/model/authzen/AccessEvaluationResponseContext.java create mode 100644 jans-core/model/src/main/java/io/jans/model/authzen/Action.java create mode 100644 jans-core/model/src/main/java/io/jans/model/authzen/Context.java create mode 100644 jans-core/model/src/main/java/io/jans/model/authzen/Resource.java create mode 100644 jans-core/model/src/main/java/io/jans/model/authzen/Subject.java create mode 100644 jans-core/script/src/main/java/io/jans/model/custom/script/type/authzen/AccessEvaluationType.java create mode 100644 jans-core/script/src/main/java/io/jans/model/custom/script/type/authzen/DummyAccessEvaluationType.java diff --git a/docs/assets/log/access-evaluation-run-log.txt b/docs/assets/log/access-evaluation-run-log.txt new file mode 100644 index 00000000000..5047ac0273b --- /dev/null +++ b/docs/assets/log/access-evaluation-run-log.txt @@ -0,0 +1,245 @@ +####################################################### +TEST: OpenID Connect Discovery +####################################################### +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +GET /.well-known/webfinger HTTP/1.1?resource=acct%3Aadmin%40happy-example.gluu.info&rel=http%3A%2F%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer HTTP/1.1 +Host: happy-example.gluu.info + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Connection: Keep-Alive +Content-Length: 207 +Content-Type: application/jrd+json;charset=iso-8859-1 +Date: Fri, 08 Nov 2024 17:15:19 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Server: Apache/2.4.52 (Ubuntu) +Set-Cookie: X-Correlation-Id=f8a91ca8-3ebb-48fb-852e-31e40b398b6d; Secure; HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{ + "subject": "acct:admin@happy-example.gluu.info", + "links": [{ + "rel": "http://openid.net/specs/connect/1.0/issuer", + "href": "https://happy-example.gluu.info" + }] +} + + +OpenID Connect Configuration +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +GET /.well-known/openid-configuration HTTP/1.1 HTTP/1.1 +Host: happy-example.gluu.info + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Connection: Keep-Alive +Content-Length: 7715 +Content-Type: application/json +Date: Fri, 08 Nov 2024 17:15:19 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Server: Apache/2.4.52 (Ubuntu) +Set-Cookie: X-Correlation-Id=474307e2-ed02-404e-bf35-a2bc60bf3421; Secure; 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://happy-example.gluu.info/jans-auth/restv1/par", + "introspection_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "introspection_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/introspection", + "claims_parameter_supported" : false, + "status_list_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/status_list", + "issuer" : "https://happy-example.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://happy-example.gluu.info/jans-auth/restv1/authorize", + "service_documentation" : "http://jans.org/docs", + "authorization_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "introspection_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "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", "room_number", "phone_number_verified", "given_name", "middle_name", "picture", "name", "phone_number", "postal_code", "region", "family_name", "jansAdminUIRole" ], + "ssa_endpoint" : "https://happy-example.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" : [ "fragment", "query.jwt", "query", "fragment.jwt", "jwt", "form_post.jwt", "form_post" ], + "backchannel_logout_session_supported" : true, + "token_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/token", + "response_types_supported" : [ "code token", "code", "code id_token", "code token id_token", "token id_token", "token", "id_token" ], + "tx_token_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "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:device_code", "refresh_token", "implicit", "password", "authorization_code", "urn:ietf:params:oauth:grant-type:uma-ticket", "urn:ietf:params:oauth:grant-type:token-exchange" ], + "ui_locales_supported" : [ "en", "bg", "de", "es", "fr", "it", "ru", "tr" ], + "prompt_values_supported" : [ "none", "login", "consent", "select_account", "create" ], + "userinfo_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/userinfo", + "access_evaluation_v1_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/access/v1/evaluation", + "authorization_challenge_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/authorization_challenge", + "op_tos_uri" : "https://happy-example.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://happy-example.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" ], + "global_token_revocation_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/global-token-revocation", + "introspection_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "tx_token_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "session_revocation_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/revoke_session", + "check_session_iframe" : "https://happy-example.gluu.info/jans-auth/opiframe.htm", + "scopes_supported" : [ "address", "introspection", "role", "access_evaluation", "https://jans.io/auth/ssa.admin", "online_access", "openid", "clientinfo", "user_name", "profile", "uma_protection", "revoke_any_token", "global_token_revocation", "https://jans.io/scim/users.write", "revoke_session", "device_sso", "https://jans.io/scim/users.read", "phone", "mobile_phone", "offline_access", "authorization_challenge", "https://jans.io/oauth/lock/audit.write", "email", "https://jans.io/oauth/lock/audit.readonly" ], + "backchannel_logout_supported" : true, + "acr_values_supported" : [ "simple_password_auth" ], + "archived_jwks_uri" : "https://happy-example.gluu.info/jans-auth/restv1/jwks/archived", + "request_object_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "device_authorization_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/device_authorization", + "display_values_supported" : [ "page", "popup" ], + "tx_token_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "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://happy-example.gluu.info/jans-auth/restv1/end_session", + "revocation_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/revoke", + "backchannel_authentication_endpoint" : "https://happy-example.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://happy-example.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://happy-example.gluu.info/jans-auth/restv1/register", + "id_token_token_binding_cnf_values_supported" : [ "tbh" ] +} + + +####################################################### +TEST: accessEvaluation_whenSubjectTypeIsAcceptedByScript_shouldGrantAccess +####################################################### +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +POST /jans-auth/restv1/register HTTP/1.1 +Host: happy-example.gluu.info +Content-Type: application/json +Accept: application/json + +{ + "grant_types" : [ "authorization_code", "refresh_token" ], + "subject_type" : "public", + "application_type" : "web", + "scope" : "access_evaluation openid profile address email phone user_name", + "minimum_acr_priority_list" : [ ], + "redirect_uris" : [ "https://happy-example.gluu.info/jans-auth-rp/home.htm", "https://client.example.com/cb", "https://client.example.com/cb1", "https://client.example.com/cb2" ], + "client_name" : "access_evaluation test", + "additional_audience" : [ ], + "response_types" : [ "code", "id_token" ] +} + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 201 +Cache-Control: no-store +Connection: Keep-Alive +Content-Length: 1664 +Content-Type: application/json +Date: Fri, 08 Nov 2024 17:15:20 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Pragma: no-cache +Server: Apache/2.4.52 (Ubuntu) +Set-Cookie: X-Correlation-Id=d7035723-e472-4cac-84c5-ef19f14fcc09; 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://happy-example.gluu.info/jans-auth/restv1/register?client_id=3cc97aab-014f-4ec9-b83a-51714e817030", + "tls_client_auth_subject_dn": "", + "run_introspection_script_before_jwt_creation": false, + "registration_access_token": "bcf42a29-d534-4ed4-a4aa-eceb4e50f472", + "client_id": "3cc97aab-014f-4ec9-b83a-51714e817030", + "token_endpoint_auth_method": "client_secret_basic", + "scope": "openid access_evaluation", + "client_secret": "aebc0eaa-f97f-4595-8ea1-ae6e541f46c6", + "client_id_issued_at": 1731086120, + "backchannel_logout_session_required": false, + "client_name": "access_evaluation test", + "par_lifetime": 600, + "spontaneous_scopes": [], + "id_token_signed_response_alg": "RS256", + "access_token_as_jwt": false, + "grant_types": [ + "refresh_token", + "authorization_code" + ], + "subject_type": "public", + "authorization_details_types": [], + "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://happy-example.gluu.info/jans-auth-rp/home.htm" + ], + "redirect_uris_regex": "", + "additional_audience": [], + "frontchannel_logout_session_required": false, + "client_secret_expires_at": 0, + "access_token_signing_alg": "RS256", + "response_types": [ + "code", + "id_token" + ] +} + +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +POST /jans-auth/restv1/access/v1/evaluation HTTP/1.1 +Host: happy-example.gluu.info +Content-Type: application/json +Authorization: Basic M2NjOTdhYWItMDE0Zi00ZWM5LWI4M2EtNTE3MTRlODE3MDMwOmFlYmMwZWFhLWY5N2YtNDU5NS04ZWExLWFlNmU1NDFmNDZjNg== + +{"subject":{"id":"alice@acmecorp.com","type":"super_admin","properties":null},"resource":{"id":"123","type":"account","properties":null},"action":{"name":"can_read","properties":{"method":"GET"}},"context":{"properties":null}} + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Connection: Keep-Alive +Content-Length: 132 +Content-Type: application/json +Date: Fri, 08 Nov 2024 17:15:21 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Server: Apache/2.4.52 (Ubuntu) +Set-Cookie: X-Correlation-Id=d4f99d9f-5b94-4863-a020-73f6fb62c5e8; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{"decision":true,"context":{"id":"9e04dd22-e980-4e54-bc04-d64a0c2e1afe","reason_admin":{"reason":"super_admin"},"reason_user":null}} + diff --git a/docs/janssen-server/auth-server/README.md b/docs/janssen-server/auth-server/README.md index b1ef8152a2f..8778cb655bd 100644 --- a/docs/janssen-server/auth-server/README.md +++ b/docs/janssen-server/auth-server/README.md @@ -34,6 +34,7 @@ FAPI-CIBA OpenID Providers for the latest results. * [Draft - Financial-grade API: Client Initiated Backchannel Authentication Profile](https://bitbucket.org/openid/fapi/src/master/Financial_API_WD_CIBA.md) * [Draft - OpenID Connect Native SSO for Mobile Apps 1.0](https://openid.net/specs/openid-connect-native-sso-1_0.html#name-authorization-request) * [Initiating User Registration via OpenID Connect 1.0](https://openid.net/specs/openid-connect-prompt-create-1_0.html) +* [The AuthZEN Authorization API 1.0](https://openid.github.io/authzen/) ** OAuth ** diff --git a/docs/janssen-server/auth-server/endpoints/access-evaluation.md b/docs/janssen-server/auth-server/endpoints/access-evaluation.md new file mode 100644 index 00000000000..c7d2cd4602f --- /dev/null +++ b/docs/janssen-server/auth-server/endpoints/access-evaluation.md @@ -0,0 +1,408 @@ +--- +tags: +- administration +- auth-server +- access-evaluation +- endpoint +--- + +# Overview + +The Jans-Auth server implements [OpenID AuthZEN Authorization API 1.0 – draft 01](https://openid.github.io/authzen/). +The AuthZEN Authorization API 1.0 specification defines a standardized interface for communication between +Policy Enforcement Points (PEPs) and Policy Decision Points (PDPs) to facilitate consistent authorization decisions across diverse systems. +It introduces an Access Evaluation API that allows PEPs to query PDPs about specific access requests, +enhancing interoperability and scalability in authorization processes. +The specification is transport-agnostic, with an initial focus on HTTPS bindings, and emphasizes secure, fine-grained, +and dynamic authorization mechanisms. + +The Access Evaluation Endpoint in the AuthZEN specification serves as a mechanism for Policy Enforcement Points (PEPs) +to request access decisions from a Policy Decision Point (PDP) for specific resources and actions. +Upon receiving a request, the endpoint evaluates the subject, resource, and action against defined policies to determine +if access should be granted, denied, or if additional information is needed. +The endpoint's responses are typically concise, aiming to provide a rapid decision that PEPs can enforce in real-time. +The goal is to provide a scalable, secure interface for dynamic and fine-grained access control across applications. + + +URL to access access evaluation 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 +``` + +`access_evaluation_v1_endpoint` claim in the response specifies the URL for access evaluation endpoint. By default, access +evaluation endpoint looks like below: + +``` +https://janssen.server.host/jans-auth/restv1/access/v1/evaluation +``` + +In order to call Access Evaluation Endpoint client must have `access_evaluation` scope. +If scope is not present AS rejects call with 401 (unauthorized) http status code. +`Authorization` header must contain valid `access_token` with `access_evaluation` scope granted to it. +Otherwise it's possible to use `Basic` token with encoded client credentials if set +`accessEvaluationAllowBasicClientAuthorization` AS configuration property to `true`. + +- Bearer token : `Authorization: Bearer ` +- Basic authorization : `Authorization: Basic ` + + +More information about request and response of the Access Evaluation 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#/access-evaluation). + +Sample request +``` +POST /jans-auth/restv1/access/v1/evaluation HTTP/1.1 +Host: happy-example.gluu.info +Content-Type: application/json +Authorization: Basic M2NjOTdhYWItMDE0Zi00ZWM5LWI4M2EtNTE3MTRlODE3MDMwOmFlYmMwZWFhLWY5N2YtNDU5NS04ZWExLWFlNmU1NDFmNDZjNg== + +{ + "subject": { + "id": "alice@acmecorp.com", + "type": "super_admin", + "properties": null + }, + "resource": { + "id": "123", + "type": "account", + "properties": null + }, + "action": { + "name": "can_read", + "properties": { + "method": "GET" + } + }, + "context": { + "properties": null + } +} +``` + +Sample successful response with `authorization_code`. +``` +HTTP/1.1 200 +Content-Type: application/json + +{ + "decision":true, + "context": { + "id":"9e04dd22-e980-4e54-bc04-d64a0c2e1afe", + "reason_admin":{"reason":"super_admin"}, + "reason_user":null + } +} +``` + +Sample error response +``` +HTTP/1.1 401 Unauthorized +Content-Type: application/json +Cache-Control: no-store + +{ + "error": "invalid_token" +} +``` + + +## Configuration Properties + +Access Evaluation Endpoint AS configuration: + +- **accessEvaluationScriptName** - Access evaluation custom script name. If not set AS falls back to first valid script found in database. +- **accessEvaluationAllowBasicClientAuthorization** - Allow basic client authorization for access evaluation endpoint. + +## Custom script + +AS provides `AccessEvaluationType` custom script which must be used to control Access Evaluation Endpoint behaviour. + +Use `accessEvaluationScriptName` configuration property to specify custom script. If not set AS falls back to first valid script found in database. + +Main `evaluate` method returns response which grants or denies access. + +Please see following snippet below: + +```java + @Override + public AccessEvaluationResponse evaluate(AccessEvaluationRequest request, Object scriptContext) { + + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + // 1. access http request via context.getHttpRequest() + // 2. access all access evaluation specific data directly with 'request', e.g. request.getSubject() + + // 3. perform custom validation if needed + validateResource(request.getResource()); + + // typically some internal validation must be performed here + // request data alone must not be trusted, it's just sample to demo script with endpoint + if ("super_admin".equalsIgnoreCase(request.getSubject().getType())) { + final ObjectNode reasonAdmin = objectMapper.createObjectNode(); + reasonAdmin.put("reason", "super_admin"); + + final AccessEvaluationResponseContext responseContext = new AccessEvaluationResponseContext(); + responseContext.setId(UUID.randomUUID().toString()); + responseContext.setReasonAdmin(reasonAdmin); + + return new AccessEvaluationResponse(true, responseContext); + } + return AccessEvaluationResponse.FALSE; + } +``` + +More details in [Access Evaluation Custom Script Page](../../developer/scripts/access-evaluation.md). + +Full sample script can be found [here](../../../script-catalog/access_evaluation/AccessEvaluation.java) + + +## Full successful Access Evaluation Flow sample + +``` +####################################################### +TEST: OpenID Connect Discovery +####################################################### +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +GET /.well-known/webfinger HTTP/1.1?resource=acct%3Aadmin%40happy-example.gluu.info&rel=http%3A%2F%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer HTTP/1.1 +Host: happy-example.gluu.info + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Connection: Keep-Alive +Content-Length: 207 +Content-Type: application/jrd+json;charset=iso-8859-1 +Date: Fri, 08 Nov 2024 17:15:19 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Server: Apache/2.4.52 (Ubuntu) +Set-Cookie: X-Correlation-Id=f8a91ca8-3ebb-48fb-852e-31e40b398b6d; Secure; HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{ + "subject": "acct:admin@happy-example.gluu.info", + "links": [{ + "rel": "http://openid.net/specs/connect/1.0/issuer", + "href": "https://happy-example.gluu.info" + }] +} + + +OpenID Connect Configuration +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +GET /.well-known/openid-configuration HTTP/1.1 HTTP/1.1 +Host: happy-example.gluu.info + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Connection: Keep-Alive +Content-Length: 7715 +Content-Type: application/json +Date: Fri, 08 Nov 2024 17:15:19 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Server: Apache/2.4.52 (Ubuntu) +Set-Cookie: X-Correlation-Id=474307e2-ed02-404e-bf35-a2bc60bf3421; Secure; 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://happy-example.gluu.info/jans-auth/restv1/par", + "introspection_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "introspection_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/introspection", + "claims_parameter_supported" : false, + "status_list_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/status_list", + "issuer" : "https://happy-example.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://happy-example.gluu.info/jans-auth/restv1/authorize", + "service_documentation" : "http://jans.org/docs", + "authorization_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "introspection_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "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", "room_number", "phone_number_verified", "given_name", "middle_name", "picture", "name", "phone_number", "postal_code", "region", "family_name", "jansAdminUIRole" ], + "ssa_endpoint" : "https://happy-example.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" : [ "fragment", "query.jwt", "query", "fragment.jwt", "jwt", "form_post.jwt", "form_post" ], + "backchannel_logout_session_supported" : true, + "token_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/token", + "response_types_supported" : [ "code token", "code", "code id_token", "code token id_token", "token id_token", "token", "id_token" ], + "tx_token_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "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:device_code", "refresh_token", "implicit", "password", "authorization_code", "urn:ietf:params:oauth:grant-type:uma-ticket", "urn:ietf:params:oauth:grant-type:token-exchange" ], + "ui_locales_supported" : [ "en", "bg", "de", "es", "fr", "it", "ru", "tr" ], + "prompt_values_supported" : [ "none", "login", "consent", "select_account", "create" ], + "userinfo_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/userinfo", + "access_evaluation_v1_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/access/v1/evaluation", + "authorization_challenge_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/authorization_challenge", + "op_tos_uri" : "https://happy-example.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://happy-example.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" ], + "global_token_revocation_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/global-token-revocation", + "introspection_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "tx_token_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "session_revocation_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/revoke_session", + "check_session_iframe" : "https://happy-example.gluu.info/jans-auth/opiframe.htm", + "scopes_supported" : [ "address", "introspection", "role", "access_evaluation", "https://jans.io/auth/ssa.admin", "online_access", "openid", "clientinfo", "user_name", "profile", "uma_protection", "revoke_any_token", "global_token_revocation", "https://jans.io/scim/users.write", "revoke_session", "device_sso", "https://jans.io/scim/users.read", "phone", "mobile_phone", "offline_access", "authorization_challenge", "https://jans.io/oauth/lock/audit.write", "email", "https://jans.io/oauth/lock/audit.readonly" ], + "backchannel_logout_supported" : true, + "acr_values_supported" : [ "simple_password_auth" ], + "archived_jwks_uri" : "https://happy-example.gluu.info/jans-auth/restv1/jwks/archived", + "request_object_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "device_authorization_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/device_authorization", + "display_values_supported" : [ "page", "popup" ], + "tx_token_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "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://happy-example.gluu.info/jans-auth/restv1/end_session", + "revocation_endpoint" : "https://happy-example.gluu.info/jans-auth/restv1/revoke", + "backchannel_authentication_endpoint" : "https://happy-example.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://happy-example.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://happy-example.gluu.info/jans-auth/restv1/register", + "id_token_token_binding_cnf_values_supported" : [ "tbh" ] +} + + +####################################################### +TEST: accessEvaluation_whenSubjectTypeIsAcceptedByScript_shouldGrantAccess +####################################################### +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +POST /jans-auth/restv1/register HTTP/1.1 +Host: happy-example.gluu.info +Content-Type: application/json +Accept: application/json + +{ + "grant_types" : [ "authorization_code", "refresh_token" ], + "subject_type" : "public", + "application_type" : "web", + "scope" : "access_evaluation openid profile address email phone user_name", + "minimum_acr_priority_list" : [ ], + "redirect_uris" : [ "https://happy-example.gluu.info/jans-auth-rp/home.htm", "https://client.example.com/cb", "https://client.example.com/cb1", "https://client.example.com/cb2" ], + "client_name" : "access_evaluation test", + "additional_audience" : [ ], + "response_types" : [ "code", "id_token" ] +} + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 201 +Cache-Control: no-store +Connection: Keep-Alive +Content-Length: 1664 +Content-Type: application/json +Date: Fri, 08 Nov 2024 17:15:20 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Pragma: no-cache +Server: Apache/2.4.52 (Ubuntu) +Set-Cookie: X-Correlation-Id=d7035723-e472-4cac-84c5-ef19f14fcc09; 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://happy-example.gluu.info/jans-auth/restv1/register?client_id=3cc97aab-014f-4ec9-b83a-51714e817030", + "tls_client_auth_subject_dn": "", + "run_introspection_script_before_jwt_creation": false, + "registration_access_token": "bcf42a29-d534-4ed4-a4aa-eceb4e50f472", + "client_id": "3cc97aab-014f-4ec9-b83a-51714e817030", + "token_endpoint_auth_method": "client_secret_basic", + "scope": "openid access_evaluation", + "client_secret": "aebc0eaa-f97f-4595-8ea1-ae6e541f46c6", + "client_id_issued_at": 1731086120, + "backchannel_logout_session_required": false, + "client_name": "access_evaluation test", + "par_lifetime": 600, + "spontaneous_scopes": [], + "id_token_signed_response_alg": "RS256", + "access_token_as_jwt": false, + "grant_types": [ + "refresh_token", + "authorization_code" + ], + "subject_type": "public", + "authorization_details_types": [], + "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://happy-example.gluu.info/jans-auth-rp/home.htm" + ], + "redirect_uris_regex": "", + "additional_audience": [], + "frontchannel_logout_session_required": false, + "client_secret_expires_at": 0, + "access_token_signing_alg": "RS256", + "response_types": [ + "code", + "id_token" + ] +} + +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +POST /jans-auth/restv1/access/v1/evaluation HTTP/1.1 +Host: happy-example.gluu.info +Content-Type: application/json +Authorization: Basic M2NjOTdhYWItMDE0Zi00ZWM5LWI4M2EtNTE3MTRlODE3MDMwOmFlYmMwZWFhLWY5N2YtNDU5NS04ZWExLWFlNmU1NDFmNDZjNg== + +{"subject":{"id":"alice@acmecorp.com","type":"super_admin","properties":null},"resource":{"id":"123","type":"account","properties":null},"action":{"name":"can_read","properties":{"method":"GET"}},"context":{"properties":null}} + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Connection: Keep-Alive +Content-Length: 132 +Content-Type: application/json +Date: Fri, 08 Nov 2024 17:15:21 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Server: Apache/2.4.52 (Ubuntu) +Set-Cookie: X-Correlation-Id=d4f99d9f-5b94-4863-a020-73f6fb62c5e8; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{"decision":true,"context":{"id":"9e04dd22-e980-4e54-bc04-d64a0c2e1afe","reason_admin":{"reason":"super_admin"},"reason_user":null}} + +``` diff --git a/docs/janssen-server/developer/interception-scripts.md b/docs/janssen-server/developer/interception-scripts.md index 45e1974feea..f59ca54c584 100644 --- a/docs/janssen-server/developer/interception-scripts.md +++ b/docs/janssen-server/developer/interception-scripts.md @@ -37,6 +37,7 @@ calling external APIs 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. [Access Evaluation](./scripts/access-evaluation.md) 1. [Authz Detail](./scripts/authz-detail.md) 1. [Create User](./scripts/create-user.md) 1. [Select Account](./scripts/select-account.md) diff --git a/docs/janssen-server/developer/scripts/README.md b/docs/janssen-server/developer/scripts/README.md index f8247d127cd..e69344737e0 100644 --- a/docs/janssen-server/developer/scripts/README.md +++ b/docs/janssen-server/developer/scripts/README.md @@ -35,6 +35,7 @@ overridden to implement your business case. | [Introspection](../../../script-catalog/introspection/introspection.md) | Introspection scripts allows to modify response of Introspection Endpoint spec and present additional meta information surrounding the token. | | [Post Authentication](../../../script-catalog/post_authn/post-authentication.md) | | | [Authorization Challenge](../../../script-catalog/authorization_challenge/authorization-challenge.md) | | +| [Access Evaluation](../../../script-catalog/access_evaluation/access-evaluation.md) | Access Evaluation custom script for Access Evaluation Endpoint (AuthZEN) | | [Select Account](../../../script-catalog/select_account/select-account.md) | | | [Resource Owner Password Credentials](../../../script-catalog/resource_owner_password_credentials/ropc.md) | | | [UMA 2 RPT Authorization Policies](../../../script-catalog/uma_rpt_policy/uma-rpt.md) | | diff --git a/docs/script-catalog/access_evaluation/AccessEvaluation.java b/docs/script-catalog/access_evaluation/AccessEvaluation.java new file mode 100644 index 00000000000..9a663fd6a94 --- /dev/null +++ b/docs/script-catalog/access_evaluation/AccessEvaluation.java @@ -0,0 +1,88 @@ +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.jans.as.server.service.external.context.ExternalScriptContext; +import io.jans.model.SimpleCustomProperty; +import io.jans.model.authzen.AccessEvaluationRequest; +import io.jans.model.authzen.AccessEvaluationResponse; +import io.jans.model.authzen.AccessEvaluationResponseContext; +import io.jans.model.authzen.Resource; +import io.jans.model.custom.script.model.CustomScript; +import io.jans.model.custom.script.type.authzen.AccessEvaluationType; +import io.jans.service.custom.script.CustomScriptManager; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.UUID; + +/** + * @author Yuriy Z + */ +public class AccessEvaluation implements AccessEvaluationType { + + private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public AccessEvaluationResponse evaluate(AccessEvaluationRequest request, Object scriptContext) { + + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + // 1. access http request via context.getHttpRequest() + // 2. access all access evaluation specific data directly with 'request', e.g. request.getSubject() + + // 3. perform custom validation if needed + validateResource(request.getResource()); + + // typically some internal validation must be performed here + // request data alone must not be trusted, it's just sample to demo script with endpoint + if ("super_admin".equalsIgnoreCase(request.getSubject().getType())) { + final ObjectNode reasonAdmin = objectMapper.createObjectNode(); + reasonAdmin.put("reason", "super_admin"); + + final AccessEvaluationResponseContext responseContext = new AccessEvaluationResponseContext(); + responseContext.setId(UUID.randomUUID().toString()); + responseContext.setReasonAdmin(reasonAdmin); + + return new AccessEvaluationResponse(true, responseContext); + } + return AccessEvaluationResponse.FALSE; + } + + private void validateResource(Resource resource) { + // sample for custom validation + if (resource.getType().equalsIgnoreCase("file")) { + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity("{\n" + + " \"error\": \"invalid_resource_type\",\n" + + " \"error_description\": \"Resource type 'file' is not allowed.\"\n" + + "}") + .build()); + } + } + + @Override + public boolean init(Map configurationAttributes) { + scriptLogger.info("Initialized AccessEvaluation Java custom script."); + return true; + } + + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + scriptLogger.info("Initialized AccessEvaluation Java custom script."); + return true; + } + + @Override + public boolean destroy(Map configurationAttributes) { + scriptLogger.info("Destroyed AccessEvaluation Java custom script."); + return false; + } + + @Override + public int getApiVersion() { + return 11; + } +} diff --git a/docs/script-catalog/access_evaluation/access-evaluation.md b/docs/script-catalog/access_evaluation/access-evaluation.md new file mode 100644 index 00000000000..c6b9ac4bc07 --- /dev/null +++ b/docs/script-catalog/access_evaluation/access-evaluation.md @@ -0,0 +1,172 @@ +--- +tags: + - administration + - developer + - script-catalog +--- + +# Access Evaluation Custom Script + +## Overview + +The Jans-Auth server implements [OpenID AuthZEN Authorization API 1.0 – draft 01](https://openid.github.io/authzen/). +The AuthZEN Authorization API 1.0 specification defines a standardized interface for communication between +Policy Enforcement Points (PEPs) and Policy Decision Points (PDPs) to facilitate consistent authorization decisions across diverse systems. +It introduces an Access Evaluation API that allows PEPs to query PDPs about specific access requests, +enhancing interoperability and scalability in authorization processes. +The specification is transport-agnostic, with an initial focus on HTTPS bindings, and emphasizes secure, fine-grained, +and dynamic authorization mechanisms. + +This script is used to control Access Evaluation Endpoint described in specification. + +## Behavior + +The Access Evaluation Endpoint in the AuthZEN specification serves as a mechanism for Policy Enforcement Points (PEPs) +to request access decisions from a Policy Decision Point (PDP) for specific resources and actions. +Upon receiving a request, the endpoint evaluates the subject, resource, and action against defined policies to determine +if access should be granted, denied, or if additional information is needed. +The endpoint's responses are typically concise, aiming to provide a rapid decision that PEPs can enforce in real-time. +The goal is to provide a scalable, secure interface for dynamic and fine-grained access control across applications. + +During Access Evaluation request processing `Access Evaluation` custom script is executed. +Name of the script must be specified by `accessEvaluationScriptName` configuration property. +If AS can't find such script or if configuration property is not specified then server executes first script it finds on database. +AS comes with sample demo script which shows simple example of custom validation and granting access. + +**Sample request** +``` +POST /jans-auth/restv1/access/v1/evaluation HTTP/1.1 +Host: happy-example.gluu.info +Content-Type: application/json +Authorization: Basic M2NjOTdhYWItMDE0Zi00ZWM5LWI4M2EtNTE3MTRlODE3MDMwOmFlYmMwZWFhLWY5N2YtNDU5NS04ZWExLWFlNmU1NDFmNDZjNg== + +{"subject":{"id":"alice@acmecorp.com","type":"super_admin","properties":null},"resource":{"id":"123","type":"account","properties":null},"action":{"name":"can_read","properties":{"method":"GET"}},"context":{"properties":null}} +``` + + + +## Interface +The Access Evaluation script implements the [AccessEvaluationType](https://github.com/JanssenProject/jans/blob/main/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzen/AccessEvaluationType.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 evaluation(self, context)`| Called when the request is received and contains main logic of evaluation. It must return `AccessEvaluationResponse`. | + +`evaluation` method returns `AccessEvaluationResponse` which indicates to RP whether to grant access or deny it. + + +### 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) | + + +## Sample Demo Custom Script + +### Script Type: Java + +```java +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.jans.as.server.service.external.context.ExternalScriptContext; +import io.jans.model.SimpleCustomProperty; +import io.jans.model.authzen.AccessEvaluationRequest; +import io.jans.model.authzen.AccessEvaluationResponse; +import io.jans.model.authzen.AccessEvaluationResponseContext; +import io.jans.model.authzen.Resource; +import io.jans.model.custom.script.model.CustomScript; +import io.jans.model.custom.script.type.authzen.AccessEvaluationType; +import io.jans.service.custom.script.CustomScriptManager; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.UUID; + +/** + * @author Yuriy Z + */ +public class AccessEvaluation implements AccessEvaluationType { + + private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public AccessEvaluationResponse evaluate(AccessEvaluationRequest request, Object scriptContext) { + + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + // 1. access http request via context.getHttpRequest() + // 2. access all access evaluation specific data directly with 'request', e.g. request.getSubject() + + // 3. perform custom validation if needed + validateResource(request.getResource()); + + // typically some internal validation must be performed here + // request data alone must not be trusted, it's just sample to demo script with endpoint + if ("super_admin".equalsIgnoreCase(request.getSubject().getType())) { + final ObjectNode reasonAdmin = objectMapper.createObjectNode(); + reasonAdmin.put("reason", "super_admin"); + + final AccessEvaluationResponseContext responseContext = new AccessEvaluationResponseContext(); + responseContext.setId(UUID.randomUUID().toString()); + responseContext.setReasonAdmin(reasonAdmin); + + return new AccessEvaluationResponse(true, responseContext); + } + return AccessEvaluationResponse.FALSE; + } + + private void validateResource(Resource resource) { + // sample for custom validation + if (resource.getType().equalsIgnoreCase("file")) { + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity("{\n" + + " \"error\": \"invalid_resource_type\",\n" + + " \"error_description\": \"Resource type 'file' is not allowed.\"\n" + + "}") + .build()); + } + } + + @Override + public boolean init(Map configurationAttributes) { + scriptLogger.info("Initialized AccessEvaluation Java custom script."); + return true; + } + + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + scriptLogger.info("Initialized AccessEvaluation Java custom script."); + return true; + } + + @Override + public boolean destroy(Map configurationAttributes) { + scriptLogger.info("Destroyed AccessEvaluation Java custom script."); + return false; + } + + @Override + public int getApiVersion() { + return 11; + } +} + +``` + + +## Sample Scripts +- [Access Evaluation](../../../script-catalog/access_evaluation/AccessEvaluation.java) diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/AccessEvaluationClient.java b/jans-auth-server/client/src/main/java/io/jans/as/client/AccessEvaluationClient.java new file mode 100644 index 00000000000..c341f320b45 --- /dev/null +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/AccessEvaluationClient.java @@ -0,0 +1,54 @@ +package io.jans.as.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import org.apache.log4j.Logger; + +/** + * @author Yuriy Z + */ +public class AccessEvaluationClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(AccessEvaluationClient.class); + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public AccessEvaluationClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.POST; + } + + public AccessEvaluationClientResponse exec(AccessEvaluationClientRequest request) { + setRequest(request); + return exec(); + } + + public AccessEvaluationClientResponse exec() { + initClient(); + + Invocation.Builder clientRequest = webTarget.request(); + + new ClientAuthnEnabler(clientRequest, requestForm).exec(request); + + clientRequest.header("Content-Type", request.getContentType()); + + try { + String jsonString = MAPPER.writeValueAsString(request.getRequest()); + clientResponse = clientRequest.buildPost(Entity.json(jsonString)).invoke(); + + final AccessEvaluationClientResponse response = new AccessEvaluationClientResponse(clientResponse); + setResponse(response); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return getResponse(); + } +} diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/AccessEvaluationClientRequest.java b/jans-auth-server/client/src/main/java/io/jans/as/client/AccessEvaluationClientRequest.java new file mode 100644 index 00000000000..b4baa62da0c --- /dev/null +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/AccessEvaluationClientRequest.java @@ -0,0 +1,63 @@ +package io.jans.as.client; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jans.as.model.util.QueryBuilder; +import io.jans.model.authzen.AccessEvaluationRequest; +import org.apache.log4j.Logger; + +/** + * @author Yuriy Z + */ +public class AccessEvaluationClientRequest extends ClientAuthnRequest implements IsJsonRequest { + + private static final Logger LOG = Logger.getLogger(AccessEvaluationClientRequest.class); + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private AccessEvaluationRequest request; + + public AccessEvaluationClientRequest() { + setContentType("application/json"); + } + + public AccessEvaluationRequest getRequest() { + return request; + } + + public AccessEvaluationClientRequest setRequest(AccessEvaluationRequest request) { + this.request = request; + return this; + } + + @Override + public String getQueryString() { + QueryBuilder builder = QueryBuilder.instance(); + if (request == null) { + return builder.toString(); + } + + appendClientAuthnToQuery(builder); + for (String key : getCustomParameters().keySet()) { + builder.append(key, getCustomParameters().get(key)); + } + + return builder.toString(); + } + + @Override + public String toString() { + return "AccessEvaluationClientRequest{" + + "request=" + request + + "} " + super.toString(); + } + + @Override + public String asJson() { + try { + return request != null ? MAPPER.writeValueAsString(request) : ""; + } catch (JsonProcessingException e) { + LOG.error("Failed to serialize request", e); + return ""; + } + } +} diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/AccessEvaluationClientResponse.java b/jans-auth-server/client/src/main/java/io/jans/as/client/AccessEvaluationClientResponse.java new file mode 100644 index 00000000000..c423d3bb413 --- /dev/null +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/AccessEvaluationClientResponse.java @@ -0,0 +1,52 @@ +package io.jans.as.client; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jans.model.authzen.AccessEvaluationResponse; +import jakarta.ws.rs.core.Response; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; + +/** + * @author Yuriy Z + */ +public class AccessEvaluationClientResponse extends BaseResponse { + + private static final Logger LOG = Logger.getLogger(AccessEvaluationClientResponse.class); + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private AccessEvaluationResponse response; + + public AccessEvaluationClientResponse(Response clientResponse) { + super(clientResponse); + injectDataFromJson(entity); + } + + public void injectDataFromJson(String json) { + if (StringUtils.isBlank(json)) { + return; + } + + try { + response = MAPPER.readValue(json, AccessEvaluationResponse.class); + } catch (JsonProcessingException e) { + LOG.error("Failed to read json: " + json, e); + } + } + + public AccessEvaluationResponse getResponse() { + return response; + } + + public AccessEvaluationClientResponse setResponse(AccessEvaluationResponse response) { + this.response = response; + return this; + } + + @Override + public String toString() { + return "AccessEvaluationClientResponse{" + + "response=" + response + + "} " + super.toString(); + } +} diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/BaseClient.java b/jans-auth-server/client/src/main/java/io/jans/as/client/BaseClient.java index 42661dba886..39286737047 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/BaseClient.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/BaseClient.java @@ -11,12 +11,6 @@ import io.jans.as.model.common.HasParamName; import io.jans.as.model.config.Constants; import io.jans.as.model.util.Util; -import org.apache.commons.lang.StringUtils; -import org.apache.log4j.Logger; -import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; -import org.jboss.resteasy.client.jaxrs.ResteasyClient; -import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; - import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.Invocation.Builder; @@ -25,6 +19,12 @@ import jakarta.ws.rs.core.Form; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; + import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; @@ -148,6 +148,8 @@ public String getRequestAsString() { sb.append("\n"); if (request instanceof RegisterRequest && ((RegisterRequest) request).hasJwtRequestAsString()) { sb.append(((RegisterRequest) request).getJwtRequestAsString()); + } else if (request instanceof IsJsonRequest) { + sb.append(((IsJsonRequest) request).asJson()); } else { sb.append(request.getQueryString()); } diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/IsJsonRequest.java b/jans-auth-server/client/src/main/java/io/jans/as/client/IsJsonRequest.java new file mode 100644 index 00000000000..83216a87570 --- /dev/null +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/IsJsonRequest.java @@ -0,0 +1,8 @@ +package io.jans.as.client; + +/** + * @author Yuriy Z + */ +public interface IsJsonRequest { + String asJson(); +} 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 a051b69b233..f15e415ae3d 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 @@ -106,6 +106,7 @@ public static void parse(String json, OpenIdConfigurationResponse response) { response.setAuthorizationEndpoint(jsonObj.optString(AUTHORIZATION_ENDPOINT, null)); response.setAuthorizationChallengeEndpoint(jsonObj.optString(AUTHORIZATION_CHALLENGE_ENDPOINT, null)); response.setStatusListEndpoint(jsonObj.optString(STATUS_LIST_ENDPOINT, null)); + response.setAccessEvaluationV1Endpoint(jsonObj.optString(ACCESS_EVALUATION_V1_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 3c52d33c889..7af28a393c9 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 @@ -29,6 +29,7 @@ public class OpenIdConfigurationResponse extends BaseResponse implements Seriali private String authorizationEndpoint; private String authorizationChallengeEndpoint; private String statusListEndpoint; + private String accessEvaluationV1Endpoint; private String tokenEndpoint; private String revocationEndpoint; private String sessionRevocationEndpoint; @@ -259,6 +260,26 @@ public void setStatusListEndpoint(String statusListEndpoint) { this.statusListEndpoint = statusListEndpoint; } + + /** + * Gets access evaluation v1 endpoint + * + * @return access evaluation v1 endpoint + */ + public String getAccessEvaluationV1Endpoint() { + return accessEvaluationV1Endpoint; + } + + /** + * Sets access evaluation v1 endpoint + * @param accessEvaluationV1Endpoint access evaluation v1 endpoint + * @return response + */ + public OpenIdConfigurationResponse setAccessEvaluationV1Endpoint(String accessEvaluationV1Endpoint) { + this.accessEvaluationV1Endpoint = accessEvaluationV1Endpoint; + return this; + } + /** * Returns the URL of the Token endpoint. * @@ -1329,6 +1350,7 @@ public String toString() { ", authorizationEndpoint='" + authorizationEndpoint + '\'' + ", authorizationChallengeEndpoint='" + authorizationChallengeEndpoint + '\'' + ", statusListEndpoint='" + statusListEndpoint + '\'' + + ", accessEvaluationV1Endpoint='" + accessEvaluationV1Endpoint + '\'' + ", 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 e6031ece2ef..c88d3d7a68a 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 @@ -78,10 +78,6 @@ import static org.testng.Assert.*; -/** - * @author Javier Rojas Blum - * @version August 26, 2021 - */ public abstract class BaseTest { public static final boolean ENABLE_REDIRECT_TO_LOGIN_PAGE = StringHelper.toBoolean(System.getProperty("gluu.enable-redirect", "false"), false); @@ -95,6 +91,7 @@ public abstract class BaseTest { protected String tokenEndpoint; protected String tokenRevocationEndpoint; protected String statusListEndpoint; + protected String accessEvaluationV1Endpoint; protected String userInfoEndpoint; protected String clientInfoEndpoint; protected String checkSessionIFrame; @@ -313,6 +310,15 @@ public void setStatusListEndpoint(String statusListEndpoint) { this.statusListEndpoint = statusListEndpoint; } + public String getAccessEvaluationV1Endpoint() { + return accessEvaluationV1Endpoint; + } + + public BaseTest setAccessEvaluationV1Endpoint(String accessEvaluationV1Endpoint) { + this.accessEvaluationV1Endpoint = accessEvaluationV1Endpoint; + return this; + } + public String getTokenEndpoint() { return tokenEndpoint; } @@ -1015,6 +1021,7 @@ public void discovery(ITestContext context) throws Exception { authorizationEndpoint = response.getAuthorizationEndpoint(); authorizationChallengeEndpoint = response.getAuthorizationChallengeEndpoint(); statusListEndpoint = response.getStatusListEndpoint(); + accessEvaluationV1Endpoint = response.getAccessEvaluationV1Endpoint(); tokenEndpoint = response.getTokenEndpoint(); tokenRevocationEndpoint = response.getRevocationEndpoint(); userInfoEndpoint = response.getUserInfoEndpoint(); @@ -1040,6 +1047,7 @@ public void discovery(ITestContext context) throws Exception { authorizationEndpoint = context.getCurrentXmlTest().getParameter("authorizationEndpoint"); authorizationChallengeEndpoint = context.getCurrentXmlTest().getParameter("authorizationChallengeEndpoint"); statusListEndpoint = context.getCurrentXmlTest().getParameter("statusListEndpoint"); + accessEvaluationV1Endpoint = context.getCurrentXmlTest().getParameter("accessEvaluationV1Endpoint"); 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/ws/rs/AccessEvaluationHttpTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/AccessEvaluationHttpTest.java new file mode 100644 index 00000000000..994415d4361 --- /dev/null +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/AccessEvaluationHttpTest.java @@ -0,0 +1,164 @@ +package io.jans.as.client.ws.rs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jans.as.client.*; +import io.jans.as.client.client.AssertBuilder; +import io.jans.as.model.common.AuthenticationMethod; +import io.jans.as.model.common.GrantType; +import io.jans.as.model.common.ResponseType; +import io.jans.as.model.common.SubjectType; +import io.jans.as.model.register.ApplicationType; +import io.jans.model.authzen.AccessEvaluationRequest; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.testng.Assert.assertTrue; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNotNull; + +/** + * Access Evaluation Endpoint HTTP Test + * + * @author Yuriy Z + */ +public class AccessEvaluationHttpTest extends BaseTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** + * Positive case for access evaluation (accepted by script if subject.type=super_admin) + */ + @Parameters({"redirectUris"}) + @Test + public void accessEvaluation_whenSubjectTypeIsAcceptedByScript_shouldGrantAccess( + final String redirectUris) throws Exception { + showTitle("accessEvaluation_whenSubjectTypeIsAcceptedByScript_shouldGrantAccess"); + assertNotNull("access_evaluation_v1_endpoint is not set in discovery", accessEvaluationV1Endpoint); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List grantTypes = Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN); + + List scopes = Arrays.asList("access_evaluation", "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 with subject.type=super_admin (in sample demo custom script on AS side we grant access in this case) + String evaluationRequestJson = "" + + "{\n" + + " \"subject\": {\n" + + " \"type\": \"super_admin\",\n" + + " \"id\": \"alice@acmecorp.com\"\n" + + " },\n" + + " \"resource\": {\n" + + " \"type\": \"account\",\n" + + " \"id\": \"123\"\n" + + " },\n" + + " \"action\": {\n" + + " \"name\": \"can_read\",\n" + + " \"properties\": {\n" + + " \"method\": \"GET\"\n" + + " }\n" + + " },\n" + + " \"context\": {\n" + + " \"time\": \"1985-10-26T01:22-07:00\"\n" + + " }\n" + + "}"; + + AccessEvaluationClientRequest evaluationRequest = new AccessEvaluationClientRequest(); + evaluationRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + evaluationRequest.setAuthUsername(clientId); + evaluationRequest.setAuthPassword(clientSecret); + evaluationRequest.setRequest(MAPPER.readValue(evaluationRequestJson, AccessEvaluationRequest.class)); + + AccessEvaluationClient evaluationClient = new AccessEvaluationClient(accessEvaluationV1Endpoint); + final AccessEvaluationClientResponse evaluationResponse = evaluationClient.exec(evaluationRequest); + showClient(evaluationClient); + + assertNotNull(evaluationResponse); + assertTrue(evaluationResponse.getResponse().isDecision()); + } + + /** + * Negative case for access evaluation (denied by script. Script accept only subject.type=super_admin, here we send subject.type=user) + */ + @Parameters({"redirectUris"}) + @Test + public void accessEvaluation_whenSubjectTypeIsNotAcceptedByScript_shouldDenyAccess( + final String redirectUris) throws Exception { + showTitle("accessEvaluation_whenSubjectTypeIsNotAcceptedByScript_shouldDenyAccess"); + assertNotNull("access_evaluation_v1_endpoint is not set in discovery", accessEvaluationV1Endpoint); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List grantTypes = Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN); + + List scopes = Arrays.asList("access_evaluation", "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 with subject.type=user (in sample demo custom script on AS side we grant access if subject.type=super_admin, otherwise -> deny) + String evaluationRequestJson = "" + + "{\n" + + " \"subject\": {\n" + + " \"type\": \"user\",\n" + + " \"id\": \"alice@acmecorp.com\"\n" + + " },\n" + + " \"resource\": {\n" + + " \"type\": \"account\",\n" + + " \"id\": \"123\"\n" + + " },\n" + + " \"action\": {\n" + + " \"name\": \"can_read\",\n" + + " \"properties\": {\n" + + " \"method\": \"GET\"\n" + + " }\n" + + " },\n" + + " \"context\": {\n" + + " \"time\": \"1985-10-26T01:22-07:00\"\n" + + " }\n" + + "}"; + + AccessEvaluationClientRequest evaluationRequest = new AccessEvaluationClientRequest(); + evaluationRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + evaluationRequest.setAuthUsername(clientId); + evaluationRequest.setAuthPassword(clientSecret); + evaluationRequest.setRequest(MAPPER.readValue(evaluationRequestJson, AccessEvaluationRequest.class)); + + AccessEvaluationClient evaluationClient = new AccessEvaluationClient(accessEvaluationV1Endpoint); + final AccessEvaluationClientResponse evaluationResponse = evaluationClient.exec(evaluationRequest); + showClient(evaluationClient); + + assertNotNull(evaluationResponse); + assertFalse(evaluationResponse.getResponse().isDecision()); + } + + public RegisterResponse registerClient(final String redirectUris, List responseTypes, List grantTypes, List scopes) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "access_evaluation test", + 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 60613ffb8e3..92c307ff963 100644 --- a/jans-auth-server/client/src/test/resources/testng.xml +++ b/jans-auth-server/client/src/test/resources/testng.xml @@ -46,7 +46,11 @@ - + + + + + diff --git a/jans-auth-server/docs/swagger.yaml b/jans-auth-server/docs/swagger.yaml index 59e4baa968c..9f9c8ef6904 100644 --- a/jans-auth-server/docs/swagger.yaml +++ b/jans-auth-server/docs/swagger.yaml @@ -19,6 +19,43 @@ 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. paths: + /evaluation: + post: + tags: + - Access Evaluation + summary: Access Evaluation AuthZEN + description: Evaluates whether a subject is authorized to perform a specific action on a resource. + operationId: post_access_evaluation + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + subject: + $ref: '#/components/schemas/Subject' + action: + $ref: '#/components/schemas/Action' + resource: + $ref: '#/components/schemas/Resource' + context: + $ref: '#/components/schemas/Context' + required: + - subject + - action + - resource + responses: + 200: + description: Access evaluation result + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluationResponse' + 400: + description: Invalid request + 500: + description: Internal server error /authorize-challenge: post: tags: @@ -4759,6 +4796,64 @@ components: type: string y: type: string + Subject: + type: object + properties: + type: + type: string + description: Type of the subject (e.g., user, service) + id: + type: string + description: Unique identifier of the subject + properties: + type: object + additionalProperties: true + description: Additional attributes of the subject + required: + - type + - id + Action: + type: object + properties: + name: + type: string + description: Name of the action (e.g., read, write) + properties: + type: object + additionalProperties: true + description: Additional attributes of the action + required: + - name + Resource: + type: object + properties: + type: + type: string + description: Type of the resource (e.g., document, database) + id: + type: string + description: Unique identifier of the resource + properties: + type: object + additionalProperties: true + description: Additional attributes of the resource + required: + - type + - id + Context: + type: object + additionalProperties: true + description: Environmental or contextual attributes for the request + EvaluationResponse: + type: object + properties: + decision: + type: boolean + description: Result of the access evaluation (true for allow, false for deny) + context: + $ref: '#/components/schemas/Context' + required: + - decision securitySchemes: bearer: type: http diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/common/FeatureFlagType.java b/jans-auth-server/model/src/main/java/io/jans/as/model/common/FeatureFlagType.java index a2a3985c547..abc8f454d9f 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/common/FeatureFlagType.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/common/FeatureFlagType.java @@ -78,6 +78,9 @@ public enum FeatureFlagType { @DocFeatureFlag(description = "Enable/Disable Pushed Authorization Requests(PAR) feature", defaultValue = "Enabled") PAR("par"), + @DocFeatureFlag(description = "Enable/Disable Access Evaluation Endpoint", + defaultValue = "Enabled") + ACCESS_EVALUATION("access_evaluation"), @DocFeatureFlag(description = "Enable/Disable Software Statement Assertion(SSA) feature", defaultValue = "Enabled") SSA("ssa"); 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 328a08b6050..6cb03db0076 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 @@ -146,6 +146,12 @@ public class AppConfiguration implements Configuration { @DocProperty(description = "Mutual TLS (mTLS) device authorization endpoint URL") private String mtlsDeviceAuthzEndpoint; + @DocProperty(description = "Allow basic client authorization for access evaluation endpoint.", defaultValue = "false") + private Boolean accessEvaluationAllowBasicClientAuthorization; + + @DocProperty(description = "Access evaluation custom script name.") + private String accessEvaluationScriptName; + @DocProperty(description = "Boolean value true encrypts request object", defaultValue = "false") private Boolean requireRequestObjectEncryption = false; @@ -934,7 +940,7 @@ public class AppConfiguration implements Configuration { @DocProperty(description = "Force Authentication Filtker to process OPTIONS request", defaultValue = "true") private Boolean skipAuthenticationFilterOptionsMethod = true; - + @DocProperty(description = "Lock message Pub configuration", defaultValue = "false") private LockMessageConfig lockMessageConfig; @@ -3503,6 +3509,25 @@ public void setMtlsDeviceAuthzEndpoint(String mtlsDeviceAuthzEndpoint) { this.mtlsDeviceAuthzEndpoint = mtlsDeviceAuthzEndpoint; } + public Boolean getAccessEvaluationAllowBasicClientAuthorization() { + return accessEvaluationAllowBasicClientAuthorization; + } + + public AppConfiguration setAccessEvaluationAllowBasicClientAuthorization(Boolean accessEvaluationAllowBasicClientAuthorization) { + if (accessEvaluationAllowBasicClientAuthorization == null) accessEvaluationAllowBasicClientAuthorization = false; + this.accessEvaluationAllowBasicClientAuthorization = accessEvaluationAllowBasicClientAuthorization; + return this; + } + + public String getAccessEvaluationScriptName() { + return accessEvaluationScriptName; + } + + public AppConfiguration setAccessEvaluationScriptName(String accessEvaluationScriptName) { + this.accessEvaluationScriptName = accessEvaluationScriptName; + return this; + } + public List getDpopSigningAlgValuesSupported() { if (dpopSigningAlgValuesSupported == null) dpopSigningAlgValuesSupported = new ArrayList<>(); return dpopSigningAlgValuesSupported; 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 0eff46d0893..e859b2d9f66 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 @@ -19,6 +19,7 @@ private ConfigurationResponseClaim() { public static final String AUTHORIZATION_ENDPOINT = "authorization_endpoint"; public static final String AUTHORIZATION_CHALLENGE_ENDPOINT = "authorization_challenge_endpoint"; public static final String STATUS_LIST_ENDPOINT = "status_list_endpoint"; + public static final String ACCESS_EVALUATION_V1_ENDPOINT = "access_evaluation_v1_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/server/src/main/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationRestWebServiceImplV1.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationRestWebServiceImplV1.java new file mode 100644 index 00000000000..5593f3d8038 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationRestWebServiceImplV1.java @@ -0,0 +1,91 @@ +package io.jans.as.server.authzen.ws.rs; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.jans.as.model.common.FeatureFlagType; +import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.server.model.common.ExecutionContext; +import io.jans.as.server.util.ServerUtil; +import io.jans.model.authzen.AccessEvaluationRequest; +import io.jans.model.authzen.AccessEvaluationResponse; +import jakarta.inject.Inject; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.slf4j.Logger; + +/** + * @author Yuriy Z + */ +@Path("/access/v1") +public class AccessEvaluationRestWebServiceImplV1 { + + public static final String X_REQUEST_ID = "X-Request-ID"; + + @Inject + private Logger log; + + @Inject + private AccessEvaluationService accessEvaluationService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @POST + @Path("/evaluation") + @Produces({MediaType.APPLICATION_JSON}) + public Response evaluation(String requestParams, @Context HttpServletRequest httpRequest, @Context HttpServletResponse httpResponse) { + + log.trace("/evaluation - request params: {}", requestParams); + + try { + errorResponseFactory.validateFeatureEnabled(FeatureFlagType.ACCESS_EVALUATION); + + String requestId = httpRequest.getHeader(X_REQUEST_ID); + String authorization = httpRequest.getHeader("Authorization"); + + accessEvaluationService.validateAuthorization(authorization); + + AccessEvaluationRequest request = readRequest(requestParams); + + ExecutionContext executionContext = ExecutionContext.of(httpRequest, httpResponse).setRequestId(requestId); + AccessEvaluationResponse response = accessEvaluationService.evaluation(request, executionContext); + + final String responseAsString = ServerUtil.asJson(response); + + log.trace("/evaluation - response entity: {}", responseAsString); + return Response.status(Response.Status.OK) + .entity(responseAsString) + .type(MediaType.APPLICATION_JSON_TYPE) + .header(X_REQUEST_ID, requestId) + .build(); + } catch (WebApplicationException e) { + if (log.isTraceEnabled()) { + log.trace(e.getMessage(), e); + } + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .type(MediaType.APPLICATION_JSON_TYPE) + .build(); + } + } + + protected AccessEvaluationRequest readRequest(String requestParams) { + try { + return ServerUtil.createJsonMapper().readValue(requestParams, AccessEvaluationRequest.class); + } catch (JsonProcessingException e) { + String msg = String.format("Failed to parse request json: %s", requestParams); + log.error(msg, e); + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity(msg) + .type(MediaType.APPLICATION_JSON_TYPE).build()); + } + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationService.java new file mode 100644 index 00000000000..b813e8f2d52 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationService.java @@ -0,0 +1,128 @@ +package io.jans.as.server.authzen.ws.rs; + +import io.jans.as.common.model.registration.Client; +import io.jans.as.model.common.FeatureFlagType; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.server.model.common.AbstractToken; +import io.jans.as.server.model.common.AuthorizationGrant; +import io.jans.as.server.model.common.ExecutionContext; +import io.jans.as.server.service.ClientService; +import io.jans.as.server.service.ScopeService; +import io.jans.as.server.service.external.ExternalAccessEvaluationService; +import io.jans.as.server.service.token.TokenService; +import io.jans.model.authzen.AccessEvaluationRequest; +import io.jans.model.authzen.AccessEvaluationResponse; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.commons.codec.binary.Base64; +import org.slf4j.Logger; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.apache.commons.lang3.BooleanUtils.isTrue; + +/** + * @author Yuriy Z + */ +@ApplicationScoped +public class AccessEvaluationService { + + public static final String ACCESS_EVALUATION_SCOPE = "access_evaluation"; + + @Inject + private Logger log; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private ExternalAccessEvaluationService externalAccessEvaluationService; + + @Inject + private AccessEvaluationValidator accessEvaluationValidator; + + @Inject + private TokenService tokenService; + + @Inject + private ClientService clientService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ScopeService scopeService; + + public AccessEvaluationResponse evaluation(AccessEvaluationRequest request, ExecutionContext executionContext) { + errorResponseFactory.validateFeatureEnabled(FeatureFlagType.ACCESS_EVALUATION); + + accessEvaluationValidator.validateAccessEvaluationRequest(request); + + final AccessEvaluationResponse response = externalAccessEvaluationService.externalEvaluate(request, executionContext); + + log.debug("Access Evaluation response {}", response); + return response; + } + + public void validateAuthorization(String authorization) { + AuthorizationGrant grant = tokenService.getBearerAuthorizationGrant(authorization); + if (grant != null) { + final String authorizationAccessToken = tokenService.getBearerToken(authorization); + final AbstractToken accessTokenObject = grant.getAccessToken(authorizationAccessToken); + if (accessTokenObject != null && accessTokenObject.isValid()) { + if (grant.getScopes() != null && grant.getScopes().contains(ACCESS_EVALUATION_SCOPE)) { + log.debug("Authorized with bearer token."); + return; + } else { + log.error("access_token does not have {} scope.", ACCESS_EVALUATION_SCOPE); + } + } else { + log.debug("Unable to find valid access token."); + } + } else { + log.debug("Unable to find grant by bearer access token."); + } + + if (isTrue(appConfiguration.getAccessEvaluationAllowBasicClientAuthorization()) && tokenService.isBasicAuthToken(authorization)) { + log.debug("Trying to perform basic client authorization ..."); + String encodedCredentials = tokenService.getBasicToken(authorization); + + String token = new String(Base64.decodeBase64(encodedCredentials), StandardCharsets.UTF_8); + + int delim = token.indexOf(":"); + + if (delim != -1) { + String clientId = URLDecoder.decode(token.substring(0, delim), StandardCharsets.UTF_8); + String password = URLDecoder.decode(token.substring(delim + 1), StandardCharsets.UTF_8); + if (clientService.authenticate(clientId, password)) { + log.debug("Authorized with basic client authentication successfully. client_id: {}", clientId); + + final Client client = clientService.getClient(clientId); + List clientScopes = scopeService.getScopeIdsByDns(client.getScopes() != null ? Arrays.asList(client.getScopes()) : new ArrayList<>()); + if (clientScopes.contains(ACCESS_EVALUATION_SCOPE)) { + log.debug("Granted access to /evaluation endpoint. Client {} has scope {}.", clientId, ACCESS_EVALUATION_SCOPE); + return; + } else { + log.debug("Access denied to /evaluation endpoint. Client {} has no scope {}.", clientId, ACCESS_EVALUATION_SCOPE); + } + } + } + log.debug("Unable to perform basic client authorization."); + } + + final String msg = "Authorization is not valid. Please provide valid authorization in 'Authorization' header."; + log.error(msg); + throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(msg) + .build()); + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationValidator.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationValidator.java new file mode 100644 index 00000000000..13804fa9a20 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationValidator.java @@ -0,0 +1,99 @@ +package io.jans.as.server.authzen.ws.rs; + +import io.jans.model.authzen.AccessEvaluationRequest; +import io.jans.model.authzen.Action; +import io.jans.model.authzen.Resource; +import io.jans.model.authzen.Subject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +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; + +/** + * @author Yuriy Z + */ +@ApplicationScoped +public class AccessEvaluationValidator { + + @Inject + private Logger log; + + public void validateAccessEvaluationRequest(AccessEvaluationRequest request) { + validateSubject(request.getSubject()); + validateResource(request.getResource()); + validateAction(request.getAction()); + } + + public void validateSubject(Subject subject) { + if (subject == null) { + final String msg = "Invalid subject. Subject is not set"; + log.trace(msg); + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity(msg) + .type(MediaType.APPLICATION_JSON_TYPE).build()); + } + + if (StringUtils.isBlank(subject.getId())) { + final String msg = "Invalid subject. Subject id can't be blank"; + log.trace(msg); + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity(msg) + .type(MediaType.APPLICATION_JSON_TYPE).build()); + } + + if (StringUtils.isBlank(subject.getType())) { + final String msg = "Invalid subject. Subject type can't be blank"; + log.trace(msg); + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity(msg) + .type(MediaType.APPLICATION_JSON_TYPE).build()); + } + } + + public void validateAction(Action action) { + if (action == null) { + final String msg = "Invalid action. Action is not set"; + log.trace(msg); + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity(msg) + .type(MediaType.APPLICATION_JSON_TYPE).build()); + } + + if (StringUtils.isBlank(action.getName())) { + final String msg = "Invalid action. Action id can't be blank"; + log.trace(msg); + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity(msg) + .type(MediaType.APPLICATION_JSON_TYPE).build()); + } + } + + public void validateResource(Resource resource) { + if (resource == null) { + final String msg = "Invalid resource. Resource is not set"; + log.trace(msg); + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity(msg) + .type(MediaType.APPLICATION_JSON_TYPE).build()); + } + + if (StringUtils.isBlank(resource.getId())) { + final String msg = "Invalid resource. Resource id can't be blank"; + log.trace(msg); + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity(msg) + .type(MediaType.APPLICATION_JSON_TYPE).build()); + } + + if (StringUtils.isBlank(resource.getType())) { + final String msg = "Invalid resource. Resource type can't be blank"; + log.trace(msg); + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity(msg) + .type(MediaType.APPLICATION_JSON_TYPE).build()); + } + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/ExecutionContext.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/ExecutionContext.java index f54e385e1af..9a8a7dfddab 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/ExecutionContext.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/ExecutionContext.java @@ -63,6 +63,7 @@ public class ExecutionContext { private String dpop; private String certAsPem; private String deviceSecret; + private String requestId; private String nonce; private String state; @@ -90,6 +91,10 @@ public ExecutionContext(HttpServletRequest httpRequest, HttpServletResponse http this.httpResponse = httpResponse; } + public static ExecutionContext of(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + return new ExecutionContext(httpRequest, httpResponse); + } + public static ExecutionContext of(AuthzRequest authzRequest) { ExecutionContext executionContext = new ExecutionContext(); if (authzRequest == null) { @@ -154,6 +159,7 @@ public static ExecutionContext of(ExecutionContext context) { executionContext.claimsAsString = context.claimsAsString; executionContext.userSessions = context.userSessions; executionContext.auditLog = context.auditLog; + executionContext.requestId = context.requestId; executionContext.attributes.clear(); executionContext.attributes.putAll(context.attributes); @@ -406,6 +412,15 @@ public void setScopes(Set scopes) { this.scopes = scopes; } + public String getRequestId() { + return requestId; + } + + public ExecutionContext setRequestId(String requestId) { + this.requestId = requestId; + return this; + } + public String getClaimsAsString() { return claimsAsString; } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/DiscoveryService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/DiscoveryService.java index 4423b1e3477..950a2b907e9 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/DiscoveryService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/DiscoveryService.java @@ -76,6 +76,8 @@ public JSONObject process() { if (appConfiguration.isFeatureEnabled(FeatureFlagType.STATUS_LIST)) jsonObj.put(STATUS_LIST_ENDPOINT, getTokenStatusListEndpoint()); + if (appConfiguration.isFeatureEnabled(FeatureFlagType.ACCESS_EVALUATION)) + jsonObj.put(ACCESS_EVALUATION_V1_ENDPOINT, getAccessEvaluationV1Endpoint()); if (appConfiguration.isFeatureEnabled(FeatureFlagType.REVOKE_TOKEN)) jsonObj.put(REVOCATION_ENDPOINT, appConfiguration.getTokenRevocationEndpoint()); if (appConfiguration.isFeatureEnabled(FeatureFlagType.REVOKE_SESSION)) @@ -241,6 +243,10 @@ public String getTokenStatusListEndpoint() { return endpointUrl("/status_list"); } + public String getAccessEvaluationV1Endpoint() { + return endpointUrl("/access/v1/evaluation"); + } + /** * @deprecated theses params: @@ -294,6 +300,8 @@ private void addMtlsAliases(JSONObject jsonObj) { aliases.put(TOKEN_ENDPOINT, appConfiguration.getMtlsTokenEndpoint()); if (appConfiguration.isFeatureEnabled(FeatureFlagType.STATUS_LIST) && StringUtils.isNotBlank(appConfiguration.getMtlsEndSessionEndpoint())) aliases.put(STATUS_LIST_ENDPOINT, endpointUrl(appConfiguration.getMtlsEndSessionEndpoint(), "/status_list")); + if (appConfiguration.isFeatureEnabled(FeatureFlagType.ACCESS_EVALUATION) && StringUtils.isNotBlank(appConfiguration.getMtlsEndSessionEndpoint())) + aliases.put(ACCESS_EVALUATION_V1_ENDPOINT, endpointUrl(appConfiguration.getMtlsEndSessionEndpoint(), "/access/v1/evaluation")); if (StringUtils.isNotBlank(appConfiguration.getMtlsJwksUri())) aliases.put(JWKS_URI, appConfiguration.getMtlsJwksUri()); if (StringUtils.isNotBlank(appConfiguration.getMtlsCheckSessionIFrame())) 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 cd1f78a8b2b..c34891e81ee 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 @@ -9,6 +9,7 @@ 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.authzen.ws.rs.AccessEvaluationRestWebServiceImplV1; import io.jans.as.server.bcauthorize.ws.rs.BackchannelAuthorizeRestWebServiceImpl; import io.jans.as.server.bcauthorize.ws.rs.BackchannelDeviceRegistrationRestWebServiceImpl; import io.jans.as.server.clientinfo.ws.rs.ClientInfoRestWebServiceImpl; @@ -52,6 +53,7 @@ public Set> getClasses() { classes.add(AuthorizeRestWebServiceImpl.class); classes.add(AuthorizationChallengeEndpoint.class); + classes.add(AccessEvaluationRestWebServiceImplV1.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/ExternalAccessEvaluationService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAccessEvaluationService.java new file mode 100644 index 00000000000..d8161ed8071 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAccessEvaluationService.java @@ -0,0 +1,80 @@ +package io.jans.as.server.service.external; + +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.server.model.common.ExecutionContext; +import io.jans.as.server.service.external.context.ExternalScriptContext; +import io.jans.as.server.service.external.context.ExternalUpdateTokenContext; +import io.jans.model.authzen.AccessEvaluationRequest; +import io.jans.model.authzen.AccessEvaluationResponse; +import io.jans.model.custom.script.CustomScriptType; +import io.jans.model.custom.script.conf.CustomScriptConfiguration; +import io.jans.model.custom.script.type.authzen.AccessEvaluationType; +import io.jans.service.custom.script.ExternalScriptService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; + +/** + * @author Yuriy Z + */ +@ApplicationScoped +public class ExternalAccessEvaluationService extends ExternalScriptService { + + @Inject + private transient AppConfiguration appConfiguration; + + public ExternalAccessEvaluationService() { + super(CustomScriptType.ACCESS_EVALUATION); + } + + public AccessEvaluationResponse externalEvaluate(AccessEvaluationRequest request, ExecutionContext context) { + final CustomScriptConfiguration script = identifyScript(); + if (script == null) { + log.debug("Failed to identify script by resource type {}", request.getResource().getType()); + return AccessEvaluationResponse.FALSE; + } + + context.setScript(script); + + try { + log.trace("Executing 'externalEvaluate' method, script name: {}, request: {}", script.getName(), request); + + ExternalScriptContext scriptContext = ExternalUpdateTokenContext.of(context); + + AccessEvaluationType evaluationType = (AccessEvaluationType) script.getExternalType(); + AccessEvaluationResponse result = evaluationType.evaluate(request, scriptContext); + + log.trace("Finished 'externalEvaluate' method, script name: {}, result: {} for request: {}, hasWebApplicationException {}", + script.getName(), result, request, scriptContext.getWebApplicationException() != null); + + scriptContext.throwWebApplicationExceptionIfSet(); + return result; + } catch (WebApplicationException e) { + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + saveScriptError(script.getCustomScript(), e); + } + + return AccessEvaluationResponse.FALSE; + } + + private CustomScriptConfiguration identifyScript() { + final String scriptName = appConfiguration.getAccessEvaluationScriptName(); + + CustomScriptConfiguration script = StringUtils.isNotBlank(scriptName) ? getCustomScriptConfigurationByName(scriptName) : null; + if (script == null) { + log.trace("Unable to find access_evaluation script by configuration property 'accessEvaluationScriptName' {}", scriptName); + final List scripts = getCustomScriptConfigurations(); + if (scripts != null && !scripts.isEmpty()) { + log.trace("Use first access_evaluation script in database because unable to find script specified by 'accessEvaluationScriptName': {}", scriptName); + script = scripts.get(0); + } + } + log.debug("Access evaluatoin with script {}, id {}", script != null ? script.getName() : "", script != null ? script.getInum() : ""); + return script; + } +} diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationRestWebServiceImplV1Test.java b/jans-auth-server/server/src/test/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationRestWebServiceImplV1Test.java new file mode 100644 index 00000000000..47b2bec6981 --- /dev/null +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationRestWebServiceImplV1Test.java @@ -0,0 +1,52 @@ +package io.jans.as.server.authzen.ws.rs; + +import io.jans.model.authzen.AccessEvaluationRequest; +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.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.fail; + +/** + * @author Yuriy Z + */ +@Listeners(MockitoTestNGListener.class) +public class AccessEvaluationRestWebServiceImplV1Test { + + @InjectMocks + private AccessEvaluationRestWebServiceImplV1 accessEvaluationRestWebServiceImplV1; + + @Mock + private Logger log; + + @Mock + private AccessEvaluationService accessEvaluationService; + + @Mock + private ErrorResponseFactory errorResponseFactory; + + @Test + public void readRequest_withValidRequest_shouldNotRaiseException() { + final AccessEvaluationRequest request = accessEvaluationRestWebServiceImplV1.readRequest("{\"subject\": {\"id\": \"subject-id\"}}"); + assertEquals("subject-id", request.getSubject().getId()); + } + + @Test + public void readRequest_withInvalidRequest_shouldRaiseException() { + try { + accessEvaluationRestWebServiceImplV1.readRequest("{invalid json}"); + } catch (WebApplicationException e) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), e.getResponse().getStatus()); + return; + } + + fail("400 WebApplicationException was not thrown."); + } +} diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationValidatorTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationValidatorTest.java new file mode 100644 index 00000000000..4c22cc82b9d --- /dev/null +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/authzen/ws/rs/AccessEvaluationValidatorTest.java @@ -0,0 +1,91 @@ +package io.jans.as.server.authzen.ws.rs; + +import io.jans.model.authzen.AccessEvaluationRequest; +import io.jans.model.authzen.Action; +import io.jans.model.authzen.Resource; +import io.jans.model.authzen.Subject; +import jakarta.ws.rs.WebApplicationException; +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; + +/** + * @author Yuriy Z + */ +@Listeners(MockitoTestNGListener.class) +public class AccessEvaluationValidatorTest { + + @InjectMocks + private AccessEvaluationValidator accessEvaluationValidator; + + @Mock + private Logger log; + + @Test + public void validateAccessEvaluationRequest_withValidRequest_shouldNotRaiseError() { + final AccessEvaluationRequest request = new AccessEvaluationRequest(); + request.setSubject(new Subject().setId("23").setType("user")); + request.setResource(new Resource().setId("456").setType("account")); + request.setAction(new Action().setName("can_read")); + + accessEvaluationValidator.validateAccessEvaluationRequest(request); + } + + @Test(expectedExceptions = WebApplicationException.class) + public void validateAccessEvaluationRequest_withoutSubject_shouldRaiseError() { + accessEvaluationValidator.validateAccessEvaluationRequest(new AccessEvaluationRequest()); + } + + @Test(expectedExceptions = WebApplicationException.class) + public void validateAccessEvaluationRequest_withSubjectWithoutId_shouldRaiseError() { + final AccessEvaluationRequest request = new AccessEvaluationRequest(); + request.setSubject(new Subject().setType("user")); + request.setResource(new Resource().setId("456").setType("account")); + request.setAction(new Action().setName("can_read")); + + accessEvaluationValidator.validateAccessEvaluationRequest(new AccessEvaluationRequest()); + } + + @Test(expectedExceptions = WebApplicationException.class) + public void validateAccessEvaluationRequest_withSubjectWithoutType_shouldRaiseError() { + final AccessEvaluationRequest request = new AccessEvaluationRequest(); + request.setSubject(new Subject().setId("23")); + request.setResource(new Resource().setId("456").setType("account")); + request.setAction(new Action().setName("can_read")); + + accessEvaluationValidator.validateAccessEvaluationRequest(new AccessEvaluationRequest()); + } + + @Test(expectedExceptions = WebApplicationException.class) + public void validateAccessEvaluationRequest_withActionWithoutName_shouldNotRaiseError() { + final AccessEvaluationRequest request = new AccessEvaluationRequest(); + request.setSubject(new Subject().setId("23").setType("user")); + request.setResource(new Resource().setId("456").setType("account")); + request.setAction(new Action()); + + accessEvaluationValidator.validateAccessEvaluationRequest(request); + } + + @Test(expectedExceptions = WebApplicationException.class) + public void validateAccessEvaluationRequest_withResourceWithoutId_shouldNotRaiseError() { + final AccessEvaluationRequest request = new AccessEvaluationRequest(); + request.setSubject(new Subject().setId("23").setType("user")); + request.setResource(new Resource().setType("account")); + request.setAction(new Action().setName("can_read")); + + accessEvaluationValidator.validateAccessEvaluationRequest(request); + } + + @Test(expectedExceptions = WebApplicationException.class) + public void validateAccessEvaluationRequest_withResourceWithoutType_shouldNotRaiseError() { + final AccessEvaluationRequest request = new AccessEvaluationRequest(); + request.setSubject(new Subject().setId("23").setType("user")); + request.setResource(new Resource().setType("account")); + request.setAction(new Action().setName("can_read")); + + accessEvaluationValidator.validateAccessEvaluationRequest(request); + } +} diff --git a/jans-auth-server/server/src/test/resources/testng.xml b/jans-auth-server/server/src/test/resources/testng.xml index 3ec4e0d2ba7..dce1c0ba683 100644 --- a/jans-auth-server/server/src/test/resources/testng.xml +++ b/jans-auth-server/server/src/test/resources/testng.xml @@ -47,6 +47,10 @@ + + + + diff --git a/jans-core/model/src/main/java/io/jans/model/authzen/AccessEvaluationRequest.java b/jans-core/model/src/main/java/io/jans/model/authzen/AccessEvaluationRequest.java new file mode 100644 index 00000000000..b87240f3e2d --- /dev/null +++ b/jans-core/model/src/main/java/io/jans/model/authzen/AccessEvaluationRequest.java @@ -0,0 +1,70 @@ +package io.jans.model.authzen; + +import java.io.Serializable; + +/** + * @author Yuriy Z + */ +public class AccessEvaluationRequest implements Serializable { + + private Subject subject; + private Resource resource; + private Action action; + private Context context; + + public AccessEvaluationRequest() { + } + + public AccessEvaluationRequest(Subject subject, Resource resource, Action action, Context context) { + this.subject = subject; + this.resource = resource; + this.action = action; + this.context = context; + } + + public Subject getSubject() { + return subject; + } + + public Resource getResource() { + return resource; + } + + public Action getAction() { + return action; + } + + public Context getContext() { + return context; + } + + public AccessEvaluationRequest setSubject(Subject subject) { + this.subject = subject; + return this; + } + + public AccessEvaluationRequest setResource(Resource resource) { + this.resource = resource; + return this; + } + + public AccessEvaluationRequest setAction(Action action) { + this.action = action; + return this; + } + + public AccessEvaluationRequest setContext(Context context) { + this.context = context; + return this; + } + + @Override + public String toString() { + return "AccessEvaluationRequest{" + + "subject=" + subject + + ", resource=" + resource + + ", action=" + action + + ", context=" + context + + '}'; + } +} diff --git a/jans-core/model/src/main/java/io/jans/model/authzen/AccessEvaluationResponse.java b/jans-core/model/src/main/java/io/jans/model/authzen/AccessEvaluationResponse.java new file mode 100644 index 00000000000..1137af95136 --- /dev/null +++ b/jans-core/model/src/main/java/io/jans/model/authzen/AccessEvaluationResponse.java @@ -0,0 +1,53 @@ +package io.jans.model.authzen; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Yuriy Z + */ +public class AccessEvaluationResponse { + + public static final AccessEvaluationResponse FALSE = new AccessEvaluationResponse(false, null); + + public static final AccessEvaluationResponse TRUE = new AccessEvaluationResponse(true, null); + + @JsonProperty("decision") + private boolean decision = false; + + @JsonProperty("context") + private AccessEvaluationResponseContext context; + + public AccessEvaluationResponse() { + } + + public AccessEvaluationResponse(boolean decision, AccessEvaluationResponseContext context) { + this.decision = decision; + this.context = context; + } + + public AccessEvaluationResponseContext getContext() { + return context; + } + + public AccessEvaluationResponse setContext(AccessEvaluationResponseContext context) { + this.context = context; + return this; + } + + public boolean isDecision() { + return decision; + } + + public AccessEvaluationResponse setDecision(boolean decision) { + this.decision = decision; + return this; + } + + @Override + public String toString() { + return "AccessEvaluationResponse{" + + "decision=" + decision + + ", context=" + context + + '}'; + } +} diff --git a/jans-core/model/src/main/java/io/jans/model/authzen/AccessEvaluationResponseContext.java b/jans-core/model/src/main/java/io/jans/model/authzen/AccessEvaluationResponseContext.java new file mode 100644 index 00000000000..d205d8f2bff --- /dev/null +++ b/jans-core/model/src/main/java/io/jans/model/authzen/AccessEvaluationResponseContext.java @@ -0,0 +1,66 @@ +package io.jans.model.authzen; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.Serializable; + +/** + * @author Yuriy Z + */ +public class AccessEvaluationResponseContext implements Serializable { + + @JsonProperty("id") + private String id; + + @JsonProperty("reason_admin") + private JsonNode reasonAdmin; + + @JsonProperty("reason_user") + private JsonNode reasonUser; + + public AccessEvaluationResponseContext() { + } + + public AccessEvaluationResponseContext(String id, JsonNode reasonAdmin, JsonNode reasonUser) { + this.id = id; + this.reasonAdmin = reasonAdmin; + this.reasonUser = reasonUser; + } + + public String getId() { + return id; + } + + public AccessEvaluationResponseContext setId(String id) { + this.id = id; + return this; + } + + public JsonNode getReasonAdmin() { + return reasonAdmin; + } + + public AccessEvaluationResponseContext setReasonAdmin(JsonNode reasonAdmin) { + this.reasonAdmin = reasonAdmin; + return this; + } + + public JsonNode getReasonUser() { + return reasonUser; + } + + public AccessEvaluationResponseContext setReasonUser(JsonNode reasonUser) { + this.reasonUser = reasonUser; + return this; + } + + @Override + public String toString() { + return "AccessEvaluationResponseContext{" + + "id='" + id + '\'' + + ", reasonAdmin=" + reasonAdmin + + ", reasonUser=" + reasonUser + + '}'; + } +} diff --git a/jans-core/model/src/main/java/io/jans/model/authzen/Action.java b/jans-core/model/src/main/java/io/jans/model/authzen/Action.java new file mode 100644 index 00000000000..d7f6444d055 --- /dev/null +++ b/jans-core/model/src/main/java/io/jans/model/authzen/Action.java @@ -0,0 +1,46 @@ +package io.jans.model.authzen; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.Serializable; + +/** + * @author Yuriy Z + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Action implements Serializable { + + @JsonProperty("name") + private String name; + + @JsonProperty("properties") + private JsonNode properties; + + public String getName() { + return name; + } + + public Action setName(String name) { + this.name = name; + return this; + } + + public JsonNode getProperties() { + return properties; + } + + public Action setProperties(JsonNode properties) { + this.properties = properties; + return this; + } + + @Override + public String toString() { + return "Action{" + + "name='" + name + '\'' + + ", properties=" + properties + + '}'; + } +} diff --git a/jans-core/model/src/main/java/io/jans/model/authzen/Context.java b/jans-core/model/src/main/java/io/jans/model/authzen/Context.java new file mode 100644 index 00000000000..16f24f816ad --- /dev/null +++ b/jans-core/model/src/main/java/io/jans/model/authzen/Context.java @@ -0,0 +1,33 @@ +package io.jans.model.authzen; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.Serializable; + +/** + * @author Yuriy Z + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Context implements Serializable { + + @JsonProperty("properties") + private JsonNode properties; + + public JsonNode getProperties() { + return properties; + } + + public Context setProperties(JsonNode properties) { + this.properties = properties; + return this; + } + + @Override + public String toString() { + return "Context{" + + "properties=" + properties + + '}'; + } +} diff --git a/jans-core/model/src/main/java/io/jans/model/authzen/Resource.java b/jans-core/model/src/main/java/io/jans/model/authzen/Resource.java new file mode 100644 index 00000000000..111bcc6677b --- /dev/null +++ b/jans-core/model/src/main/java/io/jans/model/authzen/Resource.java @@ -0,0 +1,59 @@ +package io.jans.model.authzen; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.Serializable; + +/** + * @author Yuriy Z + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Resource implements Serializable { + + @JsonProperty("id") + private String id; + + @JsonProperty("type") + private String type; + + @JsonProperty("properties") + private JsonNode properties; + + public String getId() { + return id; + } + + public Resource setId(String id) { + this.id = id; + return this; + } + + public String getType() { + return type; + } + + public Resource setType(String type) { + this.type = type; + return this; + } + + public JsonNode getProperties() { + return properties; + } + + public Resource setProperties(JsonNode properties) { + this.properties = properties; + return this; + } + + @Override + public String toString() { + return "Resource{" + + "id='" + id + '\'' + + ", type='" + type + '\'' + + ", properties=" + properties + + '}'; + } +} diff --git a/jans-core/model/src/main/java/io/jans/model/authzen/Subject.java b/jans-core/model/src/main/java/io/jans/model/authzen/Subject.java new file mode 100644 index 00000000000..72609448760 --- /dev/null +++ b/jans-core/model/src/main/java/io/jans/model/authzen/Subject.java @@ -0,0 +1,59 @@ +package io.jans.model.authzen; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.Serializable; + +/** + * @author Yuriy Z + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Subject implements Serializable { + + @JsonProperty("id") + private String id; + + @JsonProperty("type") + private String type; + + @JsonProperty("properties") + private JsonNode properties; + + public String getId() { + return id; + } + + public Subject setId(String id) { + this.id = id; + return this; + } + + public String getType() { + return type; + } + + public Subject setType(String type) { + this.type = type; + return this; + } + + public JsonNode getProperties() { + return properties; + } + + public Subject setProperties(JsonNode properties) { + this.properties = properties; + return this; + } + + @Override + public String toString() { + return "Subject{" + + "id='" + id + '\'' + + ", type='" + type + '\'' + + ", properties=" + properties + + '}'; + } +} 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 6c664e4e652..f15bcf8f8c5 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 @@ -17,6 +17,8 @@ import io.jans.model.custom.script.type.authzchallenge.DummyAuthorizationChallengeType; import io.jans.model.custom.script.type.authzdetails.AuthzDetailType; import io.jans.model.custom.script.type.authzdetails.DummyAuthzDetail; +import io.jans.model.custom.script.type.authzen.AccessEvaluationType; +import io.jans.model.custom.script.type.authzen.DummyAccessEvaluationType; 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.ClientAuthnType; @@ -98,6 +100,8 @@ public enum CustomScriptType implements AttributeEnum { UMA_RPT_CLAIMS("uma_rpt_claims", "UMA RPT Claims", UmaRptClaimsType.class, CustomScript.class, "UmaRptClaims", new UmaDummyRptClaimsType()), UMA_CLAIMS_GATHERING("uma_claims_gathering", "UMA Claims Gathering", UmaClaimsGatheringType.class, CustomScript.class, "UmaClaimsGathering", new UmaDummyClaimsGatheringType()), + ACCESS_EVALUATION("access_evaluation", "Access Evaluation", AccessEvaluationType.class, CustomScript.class, "AccessEvaluation", + new DummyAccessEvaluationType()), CONSENT_GATHERING("consent_gathering", "Consent Gathering", ConsentGatheringType.class, CustomScript.class, "ConsentGathering", new DummyConsentGatheringType()), DYNAMIC_SCOPE("dynamic_scope", "Dynamic Scopes", DynamicScopeType.class, CustomScript.class, "DynamicScope", diff --git a/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzen/AccessEvaluationType.java b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzen/AccessEvaluationType.java new file mode 100644 index 00000000000..0709617b43e --- /dev/null +++ b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzen/AccessEvaluationType.java @@ -0,0 +1,13 @@ +package io.jans.model.custom.script.type.authzen; + +import io.jans.model.authzen.AccessEvaluationRequest; +import io.jans.model.authzen.AccessEvaluationResponse; +import io.jans.model.custom.script.type.BaseExternalType; + +/** + * @author Yuriy Z + */ +public interface AccessEvaluationType extends BaseExternalType { + + AccessEvaluationResponse evaluate(AccessEvaluationRequest request, Object context); +} diff --git a/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzen/DummyAccessEvaluationType.java b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzen/DummyAccessEvaluationType.java new file mode 100644 index 00000000000..76037999e9a --- /dev/null +++ b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzen/DummyAccessEvaluationType.java @@ -0,0 +1,37 @@ +package io.jans.model.custom.script.type.authzen; + +import io.jans.model.SimpleCustomProperty; +import io.jans.model.authzen.AccessEvaluationRequest; +import io.jans.model.authzen.AccessEvaluationResponse; +import io.jans.model.custom.script.model.CustomScript; + +import java.util.Map; + +/** + * @author Yuriy Z + */ +public class DummyAccessEvaluationType implements AccessEvaluationType { + + @Override + public boolean init(Map configurationAttributes) { + return true; + } + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + return true; + } + @Override + public boolean destroy(Map configurationAttributes) { + return true; + } + + @Override + public int getApiVersion() { + return 1; + } + + @Override + public AccessEvaluationResponse evaluate(AccessEvaluationRequest request, Object context) { + return AccessEvaluationResponse.FALSE; + } +} 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 95204032f50..8ad8b20d5d5 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 @@ -20,7 +20,8 @@ "par", "ssa", "global_token_revocation", - "status_list" + "status_list", + "access_evaluation" ], "issuer":"https://%(hostname)s", "baseEndpoint":"https://%(hostname)s/jans-auth/restv1", diff --git a/jans-linux-setup/jans_setup/templates/scopes.ldif b/jans-linux-setup/jans_setup/templates/scopes.ldif index 469a5199bc1..b750c9b511a 100644 --- a/jans-linux-setup/jans_setup/templates/scopes.ldif +++ b/jans-linux-setup/jans_setup/templates/scopes.ldif @@ -119,6 +119,17 @@ jansScopeTyp: openid objectClass: top objectClass: jansScope +dn: inum=6D93,ou=scopes,o=jans +description: Access Evaluation +displayName: access_evaluation +inum: 6D93 +jansAttrs: {"spontaneousClientId":"","spontaneousClientScopes":[],"showInConfigurationEndpoint":true} +jansDefScope: false +jansId: access_evaluation +jansScopeTyp: oauth +objectClass: top +objectClass: jansScope + dn: inum=6D91,ou=scopes,o=jans description: Introspection displayName: introspection diff --git a/jans-linux-setup/jans_setup/templates/scripts.ldif b/jans-linux-setup/jans_setup/templates/scripts.ldif index 54fcfc0e909..c8c45f9c485 100644 --- a/jans-linux-setup/jans_setup/templates/scripts.ldif +++ b/jans-linux-setup/jans_setup/templates/scripts.ldif @@ -564,6 +564,20 @@ jansRevision: 11 jansScr::%(authorization_challenge_authorizationchallenge)s jansScrTyp: authorization_challenge +dn: inum=0300-BB00,ou=scripts,o=jans +objectClass: jansCustomScr +objectClass: top +description: Sample Demo of Access Evaluation Java Script +displayName: demo_evaluation +inum: 0300-BB00 +jansEnabled: true +jansLevel: 1 +jansModuleProperty: {"value1":"location_type","value2":"db","description":""} +jansProgLng: java +jansRevision: 11 +jansScr::%(access_evaluation_accessevaluation)s +jansScrTyp: access_evaluation + dn: inum=BADA-BADA,ou=scripts,o=jans objectClass: jansCustomScr objectClass: top diff --git a/jans-linux-setup/jans_setup/templates/test/jans-auth/data/jans-auth-test-data.ldif b/jans-linux-setup/jans_setup/templates/test/jans-auth/data/jans-auth-test-data.ldif index 7c63685c6bd..389ee89134c 100644 --- a/jans-linux-setup/jans_setup/templates/test/jans-auth/data/jans-auth-test-data.ldif +++ b/jans-linux-setup/jans_setup/templates/test/jans-auth/data/jans-auth-test-data.ldif @@ -107,6 +107,11 @@ changetype: modify jansDefScope: true replace: jansDefScope +dn: inum=6D93,ou=scopes,o=jans +changetype: modify +jansDefScope: true +replace: jansDefScope + dn: inum=341A,ou=scopes,o=jans changetype: modify add: jansClaim diff --git a/mkdocs.yml b/mkdocs.yml index b5f385630ee..bda1ce79fd9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -187,6 +187,7 @@ nav: - Client Authentication: janssen-server/auth-server/endpoints/client-authn.md - Authorization: janssen-server/auth-server/endpoints/authorization.md - Authorization Challenge: janssen-server/auth-server/endpoints/authorization-challenge.md + - Access Evaluation: janssen-server/auth-server/endpoints/access-evaluation.md - Token: janssen-server/auth-server/endpoints/token.md - SSA: janssen-server/auth-server/endpoints/ssa.md - Userinfo: janssen-server/auth-server/endpoints/userinfo.md