diff --git a/docs/admin/auth-server/README.md b/docs/admin/auth-server/README.md index ba27298eeab..8752c85e4f7 100644 --- a/docs/admin/auth-server/README.md +++ b/docs/admin/auth-server/README.md @@ -55,6 +55,7 @@ FAPI-CIBA OpenID Providers for the latest results. * [RFC 9126 OAuth 2.0 Pushed Authorization Requests](https://www.rfc-editor.org/rfc/rfc9126.html) * [Draft - OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP)](https://www.ietf.org/archive/id/draft-ietf-oauth-dpop-11.html) * [Draft - JWT Response for OAuth Token Introspection](https://www.ietf.org/archive/id/draft-ietf-oauth-jwt-introspection-response-12.html) +* [OAuth 2.0 Rich Authorization Requests](https://datatracker.ietf.org/doc/html/rfc9396) ** User Managed Access (UMA) ** diff --git a/docs/admin/auth-server/authz-details/README.md b/docs/admin/auth-server/authz-details/README.md new file mode 100644 index 00000000000..655fe514cd5 --- /dev/null +++ b/docs/admin/auth-server/authz-details/README.md @@ -0,0 +1,181 @@ +--- +tags: + - administration + - auth-server + - scope + - authorization-details +--- + +## OAuth 2.0 Rich Authorization Requests + +Rich Authorization Requests introduces new `authorization_details` parameter that is used to carry fine-grained authorization data in OAuth messages. + +While `scope` is used for coarse-grained access, `authorization_details` is used for fine-grained access. + +`authorization_details` are associated with authorization and thus with client to limit what authorization can be granted within given client. + +`authorization_details` is JSON array, example: + +```json +[ + { + "type": "demo_authz_detail", + "actions": [ + "list_accounts", + "read_balances" + ], + "locations": [ + "https://example.com/accounts" + ], + "ui_representation": "Read balances and list accounts at https://example.com/accounts" + }, + { + "type":"financial-transaction", + "actions":[ + "withdraw" + ], + "identifier":"account-14-32-32-3", + "currency":"USD" + } +] +``` + +### Authorization Details Types + +`type` - is required element in single authorization detail and specifies the authorization details type as a string. +Type defines how single authorization detail is handled by both AS and RS. +Because "shape" and structure of single authorization detail can vary a lot, validation and representation logic is externalized to `AuthzDetailType` custom scripts. + +`type` defines type of authorization detail. Each such type is represented by AS `AuthzDetailType` custom scripts. +It means that for example above administrator must define two `AuthzDetailType` custom scripts with names: `demo_authz_detail` and `financial-transaction`. + +If `authorization_details` parameter is absent in request then `AuthzDetailType` custom scripts are not invoked. + +`demo_authz_detail` and `financial-transaction` `AuthzDetailType` custom scripts must be provided by administrator. + +- `demo_authz_detail` is called for all authorization details with `"type": "demo_authz_detail"` +- `financial-transaction` is called for all authorization details with `"type": "financial-transaction"` + +Sample Authorization Request +``` +POST /jans-auth/restv1/authorize HTTP/1.1 +Host: yuriyz-fond-skink.gluu.info + +response_type=code&client_id=7a29bf35-96ec-4bbd-a05c-15e1ff9f07cc&scope=openid+profile+address+email+phone+user_name&redirect_uri=https%3A%2F%2Fyuriyz-relaxed-jawfish.gluu.info%2Fjans-auth-rp%2Fhome.htm&state=6cdc7701-178c-4653-adac-5c1e9c6c4aba&nonce=b9a1ecc4-548e-475c-8b29-f019417e1aef&prompt=&ui_locales=&claims_locales=&acr_values=&request_session_id=false&authorization_details=%5B%0A++%7B%0A++++%22type%22%3A+%22demo_authz_detail%22%2C%0A++++%22actions%22%3A+%5B%0A++++++%22list_accounts%22%2C%0A++++++%22read_balances%22%0A++++%5D%2C%0A++++%22locations%22%3A+%5B%0A++++++%22https%3A%2F%2Fexample.com%2Faccounts%22%0A++++%5D%2C%0A++++%22ui_representation%22%3A+%22Read+balances+and+list+accounts+at+https%3A%2F%2Fexample.com%2Faccounts%22%0A++%7D%0A%5D +``` + +Request is rejected if request's `authorization_details` has types which does not have corresponding `AuthzDetailType` custom script. + +Check more details about [`AuthzDetailType` custom scripts](../../developer/scripts/authz-detail.md) + +### AS Metadata (Discovery) + +Metadata endpoint has `authorization_details_types_supported` which shows supported authorization details types. +Value for `authorization_details_types_supported` is populated based on valid and enabled `AuthzDetailType` insterception scripts. + +For `demo_authz_detail` and `financial-transaction` `AuthzDetailType` custom scripts enabled discovery response has: + +```text +{ + "authorization_details_types_supported" : [ "demo_authz_detail", "financial-transaction" ], + ... +} +``` + +### Client Registration + +Client registration request has new parameter `authorization_details_types` to limit authorization details types supported by client. +If request is made with `authorization_details` that has types that are not listed in client's `authorization_details_types` the request will be rejected. + +Sample registration request and response +```text +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +POST /jans-auth/restv1/register HTTP/1.1 +Host: yuriyz-relaxed-jawfish.gluu.info +Content-Type: application/json +Accept: application/json + +{ + "grant_types" : [ "authorization_code", "implicit" ], + "subject_type" : "public", + "application_type" : "web", + "authorization_details_types" : [ "demo_authz_detail" ], + "scope" : "openid profile address email phone user_name", + "minimum_acr_priority_list" : [ ], + "redirect_uris" : [ "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth-rp/home.htm", "https://client.example.com/cb", "https://client.example.com/cb1", "https://client.example.com/cb2" ], + "client_name" : "jans test app", + "additional_audience" : [ ], + "response_types" : [ "code" ] +} + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 201 +Cache-Control: no-store +Connection: Keep-Alive +Content-Length: 1653 +Content-Type: application/json +Date: Mon, 18 Dec 2023 17:59:13 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Pragma: no-cache +Set-Cookie: X-Correlation-Id=7b231c8f-5b2e-445d-b5ea-0f693c1cd7f2; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{ + "allow_spontaneous_scopes": false, + "application_type": "web", + "rpt_as_jwt": false, + "registration_client_uri": "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/register?client_id=7a29bf35-96ec-4bbd-a05c-15e1ff9f07cc", + "tls_client_auth_subject_dn": "", + "run_introspection_script_before_jwt_creation": false, + "registration_access_token": "92a40113-b27c-43b9-bf96-a222fcfe1c9c", + "client_id": "7a29bf35-96ec-4bbd-a05c-15e1ff9f07cc", + "token_endpoint_auth_method": "client_secret_basic", + "scope": "openid", + "client_secret": "1af17da1-57a3-416b-a358-c84bb0ef0fad", + "client_id_issued_at": 1702922353, + "backchannel_logout_uri": [], + "backchannel_logout_session_required": false, + "client_name": "jans test app", + "par_lifetime": 600, + "spontaneous_scopes": [], + "id_token_signed_response_alg": "RS256", + "access_token_as_jwt": false, + "grant_types": [ + "authorization_code", + "implicit" + ], + "subject_type": "public", + "authorization_details_types": ["demo_authz_detail"], + "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://yuriyz-relaxed-jawfish.gluu.info/jans-auth-rp/home.htm", + "https://client.example.com/cb" + ], + "redirect_uris_regex": "", + "additional_audience": [], + "frontchannel_logout_session_required": false, + "client_secret_expires_at": 0, + "access_token_signing_alg": "RS256", + "response_types": ["code"] +} + +``` + + + + + + + + diff --git a/docs/admin/developer/interception-scripts.md b/docs/admin/developer/interception-scripts.md index a62ae8ec436..d5826da1118 100644 --- a/docs/admin/developer/interception-scripts.md +++ b/docs/admin/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. [Authz Detail](./scripts/authz-detail.md) 1. [Select Account](./scripts/select-account.md) 1. Resource Owner Password Credentials 1. UMA 2 RPT Authorization Policies diff --git a/docs/admin/developer/scripts/authz-detail.md b/docs/admin/developer/scripts/authz-detail.md new file mode 100644 index 00000000000..fbdf2bbff7a --- /dev/null +++ b/docs/admin/developer/scripts/authz-detail.md @@ -0,0 +1,185 @@ +--- +tags: + - administration + - developer + - scripts +--- + +# Authorization Detail Custom Script (AuthzDetail) + +## Overview + +The Jans-Auth server implements [OAuth 2.0 Rich Authorization Requests](https://datatracker.ietf.org/doc/html/rfc9396). +This script is used to control/customize single authorization detail from `authorization_details` array. + +## Behavior + +In request to Authorization Endpoint and to Token Endpoint RP can specify `authorization_details` request parameter which specifies JSON array. + +```json +[ + { + "type": "demo_authz_detail", + "actions": [ + "list_accounts", + "read_balances" + ], + "locations": [ + "https://example.com/accounts" + ], + "ui_representation": "Read balances and list accounts at https://example.com/accounts" + }, + { + "type":"financial-transaction", + "actions":[ + "withdraw" + ], + "identifier":"account-14-32-32-3", + "currency":"USD", + "ui_representation": "Withdraw money from account-14-32-32-3" + } +] +``` + +`type` defines type of authorization detail. Each such type is represented by AS `AuthzDetailType` custom scripts. +It means that for example above administrator must define two `AuthzDetailType` custom scripts with names: `demo_authz_detail` and `financial-transaction`. + +If `authorization_details` parameter is absent in request then `AuthzDetailType` custom scripts are not invoked. + +`demo_authz_detail` and `financial-transaction` `AuthzDetailType` custom scripts must be provided by administrator. + +- `demo_authz_detail` is called for all authorization details with `"type": "demo_authz_detail"` +- `financial-transaction` is called for all authorization details with `"type": "financial-transaction"` + +Sample Authorization Request +``` +POST /jans-auth/restv1/authorize HTTP/1.1 +Host: yuriyz-fond-skink.gluu.info + +response_type=code&client_id=7a29bf35-96ec-4bbd-a05c-15e1ff9f07cc&scope=openid+profile+address+email+phone+user_name&redirect_uri=https%3A%2F%2Fyuriyz-relaxed-jawfish.gluu.info%2Fjans-auth-rp%2Fhome.htm&state=6cdc7701-178c-4653-adac-5c1e9c6c4aba&nonce=b9a1ecc4-548e-475c-8b29-f019417e1aef&prompt=&ui_locales=&claims_locales=&acr_values=&request_session_id=false&authorization_details=%5B%0A++%7B%0A++++%22type%22%3A+%22demo_authz_detail%22%2C%0A++++%22actions%22%3A+%5B%0A++++++%22list_accounts%22%2C%0A++++++%22read_balances%22%0A++++%5D%2C%0A++++%22locations%22%3A+%5B%0A++++++%22https%3A%2F%2Fexample.com%2Faccounts%22%0A++++%5D%2C%0A++++%22ui_representation%22%3A+%22Read+balances+and+list+accounts+at+https%3A%2F%2Fexample.com%2Faccounts%22%0A++%7D%0A%5D +``` + +## Interface +The Authorization Details script implements the [AuthzDetailType](https://github.com/JanssenProject/jans/blob/main/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzdetails/AuthzDetailType.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 validateDetail(self, context)`| Called when the request is received. Method validates single authorization detail from `authorization_details`. | +|`def getUiRepresentation(self, context)`| Called when single authorization detail from `authorization_details` has to be represented on UI as string. For example on authorization page. | + +`validateDetail` method returns true/false which indicates to server whether the validation of single authorization detail (from `authorization_details` array) is passed or failed. +If at least one element from `authorization_details` array fails validation error is returned by AS. + +`getUiRepresentation` method returns string and represents single authorization detail as string on UI. Authorization detail can have "ui_representation" json key which makes implementation as simple as following: + +```java + @Override + public String getUiRepresentation(Object scriptContext) { + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + return context.getAuthzDetail().getJsonObject().optString("ui_representation"); + } +``` + + +### 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) | + +- Get Authz Detail - `context.getAuthzDetail()` +- Get Authz Detail Type - `context.getAuthzDetail().getType()` +- Get Authz Detail JSON Object (`org.json.JSONObject`) for manipulation - `context.getAuthzDetail().getJsonObject()` +- Get full HTTP Request - `context.getHttpRequest()` + + +## Simple Use Case: validate authz details is present and return string representation + +### Script Type: Java + +```java +/* + Copyright (c) 2023, Gluu + Author: Yuriy Z + */ + +import io.jans.as.server.service.external.context.ExternalScriptContext; +import io.jans.model.SimpleCustomProperty; +import io.jans.model.custom.script.model.CustomScript; +import io.jans.model.custom.script.type.authzdetails.AuthzDetailType; +import io.jans.service.custom.script.CustomScriptManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +/** + * @author Yuriy Z + */ +public class AuthzDetail implements AuthzDetailType { + + private static final Logger log = LoggerFactory.getLogger(AuthzDetail.class); + private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class); + + /** + * All validation logic of single authorization detail must take place in this method. + * If method returns "false" AS returns error to RP. If "true" processing of request goes on. + * + * @param scriptContext script context. Authz detail can be taken as "context.getAuthzDetail()". + * @return whether single authorization detail is valid or not + */ + @Override + public boolean validateDetail(Object scriptContext) { + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + return context.getAuthzDetail() != null; + } + + /** + * Method returns single authorization detail string representation which is shown on authorization page by AS. + * + * @param scriptContext script context. Authz detail can be taken as "context.getAuthzDetail()". + * @return returns single authorization details string representation which is shown on authorization page by AS. + */ + @Override + public String getUiRepresentation(Object scriptContext) { + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + return context.getAuthzDetail().getJsonObject().optString("ui_representation"); + } + + @Override + public boolean init(Map configurationAttributes) { + scriptLogger.info("Initialized AuthzDetail Java custom script."); + return true; + } + + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + scriptLogger.info("Initialized AuthzDetail Java custom script."); + return true; + } + + @Override + public boolean destroy(Map configurationAttributes) { + scriptLogger.info("Destroyed AuthzDetail Java custom script."); + return true; + } + + @Override + public int getApiVersion() { + return 11; + } +} + +``` + + +## Sample Scripts +- [AuthzDetails](../../../script-catalog/authz_detail/AuthzDetail.java) diff --git a/docs/assets/log/authorization-details-run-log.txt b/docs/assets/log/authorization-details-run-log.txt new file mode 100644 index 00000000000..d9cad9f0c77 --- /dev/null +++ b/docs/assets/log/authorization-details-run-log.txt @@ -0,0 +1,279 @@ +####################################################### +TEST: OpenID Connect Discovery +####################################################### +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +GET /.well-known/webfinger HTTP/1.1?resource=acct%3Aadmin%40yuriyz-relaxed-jawfish.gluu.info&rel=http%3A%2F%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer HTTP/1.1 +Host: yuriyz-relaxed-jawfish.gluu.info + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Connection: Keep-Alive +Content-Length: 209 +Content-Type: application/jrd+json;charset=iso-8859-1 +Date: Mon, 18 Dec 2023 17:59:12 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=d049a147-bbc9-4622-bead-110efe63cdbc; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{ + "subject": "acct:admin@yuriyz-relaxed-jawfish.gluu.info", + "links": [{ + "rel": "http://openid.net/specs/connect/1.0/issuer", + "href": "https://yuriyz-relaxed-jawfish.gluu.info" + }] +} + + +OpenID Connect Configuration +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +GET /.well-known/openid-configuration HTTP/1.1 HTTP/1.1 +Host: yuriyz-relaxed-jawfish.gluu.info + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Connection: Keep-Alive +Content-Length: 6401 +Content-Type: application/json +Date: Mon, 18 Dec 2023 17:59:12 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=b3aee6ea-89bf-43e3-8d3f-241642402c65; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{ + "request_parameter_supported" : true, + "pushed_authorization_request_endpoint" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/par", + "introspection_endpoint" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/introspection", + "claims_parameter_supported" : false, + "issuer" : "https://yuriyz-relaxed-jawfish.gluu.info", + "userinfo_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "id_token_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "access_token_signing_alg_values_supported" : [ "none", "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "authorization_endpoint" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/authorize", + "service_documentation" : "http://jans.org/docs", + "authorization_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "claims_supported" : [ "street_address", "country", "zoneinfo", "birthdate", "role", "gender", "user_name", "formatted", "phone_mobile_number", "preferred_username", "locale", "inum", "updated_at", "post_office_box", "nickname", "preferred_language", "email", "website", "email_verified", "profile", "locality", "phone_number_verified", "room_number", "given_name", "middle_name", "picture", "name", "phone_number", "postal_code", "region", "family_name", "jansAdminUIRole" ], + "ssa_endpoint" : "https://yuriyz-relaxed-jawfish.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" : [ "form_post.jwt", "query.jwt", "form_post", "fragment", "query", "jwt", "fragment.jwt" ], + "backchannel_logout_session_supported" : true, + "token_endpoint" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/token", + "response_types_supported" : [ "id_token token code", "token code", "id_token token", "id_token", "id_token code", "token", "code" ], + "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" : [ "refresh_token", "urn:ietf:params:oauth:grant-type:uma-ticket", "password", "authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "implicit", "client_credentials", "urn:ietf:params:oauth:grant-type:device_code" ], + "ui_locales_supported" : [ "en", "bg", "de", "es", "fr", "it", "ru", "tr" ], + "userinfo_endpoint" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/userinfo", + "authorization_challenge_endpoint" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/authorization_challenge", + "op_tos_uri" : "https://yuriyz-relaxed-jawfish.gluu.info/tos", + "require_request_uri_registration" : false, + "id_token_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "frontchannel_logout_session_supported" : true, + "authorization_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "claims_locales_supported" : [ "en" ], + "clientinfo_endpoint" : "https://yuriyz-relaxed-jawfish.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" ], + "session_revocation_endpoint" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/revoke_session", + "check_session_iframe" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/opiframe.htm", + "scopes_supported" : [ "address", "introspection", "https://jans.io/auth/ssa.admin", "online_access", "user_name", "openid", "clientinfo", "profile", "uma_protection", "permission", "https://jans.io/scim/users.write", "revoke_any_token", "revoke_session", "device_sso", "https://jans.io/scim/users.read", "phone", "mobile_phone", "offline_access", "authorization_challenge", "email" ], + "backchannel_logout_supported" : true, + "acr_values_supported" : [ "simple_password_auth" ], + "request_object_encryption_enc_values_supported" : [ "A128CBC+HS256", "A256CBC+HS512", "A128GCM", "A256GCM" ], + "device_authorization_endpoint" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/device_authorization", + "display_values_supported" : [ "page", "popup" ], + "userinfo_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "require_pushed_authorization_requests" : false, + "claim_types_supported" : [ "normal" ], + "userinfo_encryption_alg_values_supported" : [ "RSA1_5", "RSA-OAEP", "A128KW", "A256KW" ], + "end_session_endpoint" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/end_session", + "authorization_details_types_supported" : [ "demo_authz_detail" ], + "revocation_endpoint" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/revoke", + "backchannel_authentication_endpoint" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/bc-authorize", + "token_endpoint_auth_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "frontchannel_logout_supported" : true, + "jwks_uri" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/jwks", + "subject_types_supported" : [ "public", "pairwise" ], + "id_token_signing_alg_values_supported" : [ "none", "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES512", "PS256", "PS384", "PS512" ], + "registration_endpoint" : "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/register", + "id_token_token_binding_cnf_values_supported" : [ "tbh" ] +} + + +####################################################### +TEST: authorizationCodeFlow +####################################################### +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +POST /jans-auth/restv1/register HTTP/1.1 +Host: yuriyz-relaxed-jawfish.gluu.info +Content-Type: application/json +Accept: application/json + +{ + "grant_types" : [ "authorization_code", "implicit" ], + "subject_type" : "public", + "application_type" : "web", + "authorization_details_types" : [ "demo_authz_detail" ], + "scope" : "openid profile address email phone user_name", + "minimum_acr_priority_list" : [ ], + "redirect_uris" : [ "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth-rp/home.htm", "https://client.example.com/cb", "https://client.example.com/cb1", "https://client.example.com/cb2" ], + "client_name" : "jans test app", + "additional_audience" : [ ], + "response_types" : [ "code" ] +} + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 201 +Cache-Control: no-store +Connection: Keep-Alive +Content-Length: 1653 +Content-Type: application/json +Date: Mon, 18 Dec 2023 17:59:13 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Pragma: no-cache +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=7b231c8f-5b2e-445d-b5ea-0f693c1cd7f2; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{ + "allow_spontaneous_scopes": false, + "application_type": "web", + "rpt_as_jwt": false, + "registration_client_uri": "https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/register?client_id=7a29bf35-96ec-4bbd-a05c-15e1ff9f07cc", + "tls_client_auth_subject_dn": "", + "run_introspection_script_before_jwt_creation": false, + "registration_access_token": "92a40113-b27c-43b9-bf96-a222fcfe1c9c", + "client_id": "7a29bf35-96ec-4bbd-a05c-15e1ff9f07cc", + "token_endpoint_auth_method": "client_secret_basic", + "scope": "openid", + "client_secret": "1af17da1-57a3-416b-a358-c84bb0ef0fad", + "client_id_issued_at": 1702922353, + "backchannel_logout_uri": [], + "backchannel_logout_session_required": false, + "client_name": "jans test app", + "par_lifetime": 600, + "spontaneous_scopes": [], + "id_token_signed_response_alg": "RS256", + "access_token_as_jwt": false, + "grant_types": [ + "authorization_code", + "implicit" + ], + "subject_type": "public", + "authorization_details_types": ["demo_authz_detail"], + "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://yuriyz-relaxed-jawfish.gluu.info/jans-auth-rp/home.htm", + "https://client.example.com/cb" + ], + "redirect_uris_regex": "", + "additional_audience": [], + "frontchannel_logout_session_required": false, + "client_secret_expires_at": 0, + "access_token_signing_alg": "RS256", + "response_types": ["code"] +} + +authenticateResourceOwnerAndGrantAccess: authorizationRequestUrl:https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/authorize?response_type=code&client_id=7a29bf35-96ec-4bbd-a05c-15e1ff9f07cc&scope=openid+profile+address+email+phone+user_name&redirect_uri=https%3A%2F%2Fyuriyz-relaxed-jawfish.gluu.info%2Fjans-auth-rp%2Fhome.htm&state=6cdc7701-178c-4653-adac-5c1e9c6c4aba&nonce=b9a1ecc4-548e-475c-8b29-f019417e1aef&prompt=&ui_locales=&claims_locales=&acr_values=&request_session_id=false&authorization_details=%5B%0A++%7B%0A++++%22type%22%3A+%22demo_authz_detail%22%2C%0A++++%22actions%22%3A+%5B%0A++++++%22list_accounts%22%2C%0A++++++%22read_balances%22%0A++++%5D%2C%0A++++%22locations%22%3A+%5B%0A++++++%22https%3A%2F%2Fexample.com%2Faccounts%22%0A++++%5D%2C%0A++++%22ui_representation%22%3A+%22Read+balances+and+list+accounts+at+https%3A%2F%2Fexample.com%2Faccounts%22%0A++%7D%0A%5D +authenticateResourceOwnerAndGrantAccess: sessionState:d436addcb63f925ccbf6ab84ad247b443d68c8689cb45d936bb1e5e3f376395b.946acbad-c2b4-4ce7-a8f9-8fb9c9fa7ab5 +authenticateResourceOwnerAndGrantAccess: sessionId:465cad0c-5670-42c9-9aa6-575c632ebd18 +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +https://yuriyz-relaxed-jawfish.gluu.info/jans-auth/restv1/authorize?response_type=code&client_id=7a29bf35-96ec-4bbd-a05c-15e1ff9f07cc&scope=openid+profile+address+email+phone+user_name&redirect_uri=https%3A%2F%2Fyuriyz-relaxed-jawfish.gluu.info%2Fjans-auth-rp%2Fhome.htm&state=6cdc7701-178c-4653-adac-5c1e9c6c4aba&nonce=b9a1ecc4-548e-475c-8b29-f019417e1aef&prompt=&ui_locales=&claims_locales=&acr_values=&request_session_id=false&authorization_details=%5B%0A++%7B%0A++++%22type%22%3A+%22demo_authz_detail%22%2C%0A++++%22actions%22%3A+%5B%0A++++++%22list_accounts%22%2C%0A++++++%22read_balances%22%0A++++%5D%2C%0A++++%22locations%22%3A+%5B%0A++++++%22https%3A%2F%2Fexample.com%2Faccounts%22%0A++++%5D%2C%0A++++%22ui_representation%22%3A+%22Read+balances+and+list+accounts+at+https%3A%2F%2Fexample.com%2Faccounts%22%0A++%7D%0A%5D + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 302 Found +Location: https://yuriyz-relaxed-jawfish.gluu.info/jans-auth-rp/home.htm?code=e8ac07ed-450f-4120-aeb7-a7351f0642a4&scope=openid&state=6cdc7701-178c-4653-adac-5c1e9c6c4aba&session_state=d436addcb63f925ccbf6ab84ad247b443d68c8689cb45d936bb1e5e3f376395b.946acbad-c2b4-4ce7-a8f9-8fb9c9fa7ab5 + +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +POST /jans-auth/restv1/token HTTP/1.1 +Host: yuriyz-relaxed-jawfish.gluu.info +Content-Type: application/x-www-form-urlencoded +Authorization: Basic N2EyOWJmMzUtOTZlYy00YmJkLWEwNWMtMTVlMWZmOWYwN2NjOjFhZjE3ZGExLTU3YTMtNDE2Yi1hMzU4LWM4NGJiMGVmMGZhZA== + +grant_type=authorization_code&code=e8ac07ed-450f-4120-aeb7-a7351f0642a4&redirect_uri=https%3A%2F%2Fyuriyz-relaxed-jawfish.gluu.info%2Fjans-auth-rp%2Fhome.htm&authorizationDetails=%5B%0A++%7B%0A++++%22type%22%3A+%22demo_authz_detail%22%2C%0A++++%22actions%22%3A+%5B%0A++++++%22list_accounts%22%2C%0A++++++%22read_balances%22%0A++++%5D%2C%0A++++%22locations%22%3A+%5B%0A++++++%22https%3A%2F%2Fexample.com%2Faccounts%22%0A++++%5D%2C%0A++++%22ui_representation%22%3A+%22Read+balances+and+list+accounts+at+https%3A%2F%2Fexample.com%2Faccounts%22%0A++%7D%0A%5D + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Cache-Control: no-store +Connection: Keep-Alive +Content-Length: 1501 +Content-Type: application/json +Date: Mon, 18 Dec 2023 17:59:20 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Pragma: no-cache +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=b3eea320-c43d-4141-96de-e8b4b7c51e36; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{"access_token":"549aa20a-f784-471f-a361-e01bafeeee8e","authorization_details":[{"locations":["https://example.com/accounts"],"ui_representation":"Read balances and list accounts at https://example.com/accounts","type":"demo_authz_detail","actions":["list_accounts","read_balances"]}],"id_token":"eyJraWQiOiJjb25uZWN0XzMyM2M5OGJlLWM5OWEtNDAzZi1hNzYwLTQ1YmRlZThlNWQ5ZV9zaWdfcnMyNTYiLCJ0eXAiOiJqd3QiLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiNEtVN3dnVXNMOGxhemh0TXN3cWdydyIsInN1YiI6Ijg4NWEwMmQwLTA5YjItNGZhNC04YTNiLWQzMTBiYjI2YWQ2MyIsImFtciI6WyItMSJdLCJpc3MiOiJodHRwczovL3l1cml5ei1yZWxheGVkLWphd2Zpc2guZ2x1dS5pbmZvIiwibm9uY2UiOiJiOWExZWNjNC01NDhlLTQ3NWMtOGIyOS1mMDE5NDE3ZTFhZWYiLCJzaWQiOiI4N2ZlNDg1Zi1mYWY4LTQwMmMtYjMyNC1mY2VmMjcxZWRkOTciLCJqYW5zT3BlbklEQ29ubmVjdFZlcnNpb24iOiJvcGVuaWRjb25uZWN0LTEuMCIsImF1ZCI6IjdhMjliZjM1LTk2ZWMtNGJiZC1hMDVjLTE1ZTFmZjlmMDdjYyIsInJhbmRvbSI6ImYwYjFiZTEwLTQ5Y2MtNDliNS1hNzg2LTM0MDg1ZGY3OWExZiIsImFjciI6InNpbXBsZV9wYXNzd29yZF9hdXRoIiwiY19oYXNoIjoiaVlMTndmUHF2S1oyNTd4U1dtSTZMQSIsImF1dGhfdGltZSI6MTcwMjkyMjM1OCwiZXhwIjoxNzAyOTI1OTYwLCJncmFudCI6ImF1dGhvcml6YXRpb25fY29kZSIsImlhdCI6MTcwMjkyMjM2MH0.HobzVNnM07ASUJibpgD5sOubtDBwHXvxVEJEl-ZtU2-78urvXLBcL2eMWHEkzvir95BN7y0O9OzJ6s5Vq-PEQYJOyBs9RQZB4RSkBzNlKsXKpOrOaWkqoZ7u6y-hUF7QsIPuRNNjXPV06ixkInLN6cdLyX-W0TfH_nxzqgBVH3tFF9fYURh6PVf2ExCqDdMW4OWo7Gh76MJcykRJrdP61Zx1zvgWnsbGCHimZkhTnNGdz4SJl-EgGDgMYKnVWOLC2SZ8PA_yMeNe6DGqquLfdDVi4DNukFOb_9MfGAIVIkacqjjpr8TQ-wU8JpmYopvYsg_siXBmt-fYR3E01BcBqA","token_type":"Bearer","expires_in":299} + +------------------------------------------------------- +REQUEST: +------------------------------------------------------- +GET /jans-auth/restv1/userinfo HTTP/1.1 HTTP/1.1 +Host: yuriyz-relaxed-jawfish.gluu.info +Authorization: Bearer 549aa20a-f784-471f-a361-e01bafeeee8e + +------------------------------------------------------- +RESPONSE: +------------------------------------------------------- +HTTP/1.1 200 +Cache-Control: no-store, private +Connection: Keep-Alive +Content-Length: 46 +Content-Type: application/json;charset=utf-8 +Date: Mon, 18 Dec 2023 17:59:20 GMT +Expires: Thu, 01 Jan 1970 00:00:00 GMT +Keep-Alive: timeout=5, max=100 +Pragma: no-cache +Server: Apache/2.4.41 (Ubuntu) +Set-Cookie: X-Correlation-Id=243d3a92-e7fd-4aa9-80b1-b6050e87b490; Secure; HttpOnly;HttpOnly +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Xss-Protection: 1; mode=block + +{"sub":"885a02d0-09b2-4fa4-8a3b-d310bb26ad63"} + +Introspection response for access_token: 549aa20a-f784-471f-a361-e01bafeeee8e +IntrospectionResponse{active=true, scope=[openid], authorizationDetails=[{"locations":["https://example.com/accounts"],"ui_representation":"Read balances and list accounts at https://example.com/accounts","type":"demo_authz_detail","actions":["list_accounts","read_balances"]}], clientId='7a29bf35-96ec-4bbd-a05c-15e1ff9f07cc', username='admin', tokenType='Bearer', expiresAt=1702922660, issuedAt=1702922360, subject='885a02d0-09b2-4fa4-8a3b-d310bb26ad63', audience='7a29bf35-96ec-4bbd-a05c-15e1ff9f07cc', issuer='https://yuriyz-relaxed-jawfish.gluu.info', jti='null', acr='simple_password_auth', authTime='1702922358'} diff --git a/docs/script-catalog/authz_detail/AuthzDetail.java b/docs/script-catalog/authz_detail/AuthzDetail.java new file mode 100644 index 00000000000..3c60492a373 --- /dev/null +++ b/docs/script-catalog/authz_detail/AuthzDetail.java @@ -0,0 +1,71 @@ +/* + Copyright (c) 2023, Gluu + Author: Yuriy Z + */ + +import io.jans.as.server.service.external.context.ExternalScriptContext; +import io.jans.model.SimpleCustomProperty; +import io.jans.model.custom.script.model.CustomScript; +import io.jans.model.custom.script.type.authzdetails.AuthzDetailType; +import io.jans.service.custom.script.CustomScriptManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +/** + * @author Yuriy Z + */ +public class AuthzDetail implements AuthzDetailType { + + private static final Logger log = LoggerFactory.getLogger(AuthzDetail.class); + private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class); + + /** + * All validation logic of single authorization detail must take place in this method. + * If method returns "false" AS returns error to RP. If "true" processing of request goes on. + * + * @param scriptContext script context. Authz detail can be taken as "context.getAuthzDetail()". + * @return whether single authorization detail is valid or not + */ + @Override + public boolean validateDetail(Object scriptContext) { + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + return context.getAuthzDetail() != null; + } + + /** + * Method returns single authorization detail string representation which is shown on authorization page by AS. + * + * @param scriptContext script context. Authz detail can be taken as "context.getAuthzDetail()". + * @return returns single authorization details string representation which is shown on authorization page by AS. + */ + @Override + public String getUiRepresentation(Object scriptContext) { + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + return context.getAuthzDetail().getJsonObject().optString("ui_representation"); + } + + @Override + public boolean init(Map configurationAttributes) { + scriptLogger.info("Initialized AuthzDetail Java custom script."); + return true; + } + + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + scriptLogger.info("Initialized AuthzDetail Java custom script."); + return true; + } + + @Override + public boolean destroy(Map configurationAttributes) { + scriptLogger.info("Destroyed AuthzDetail Java custom script."); + return true; + } + + @Override + public int getApiVersion() { + return 11; + } +} diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizationRequest.java b/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizationRequest.java index 47fac3777da..35272eba3f0 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizationRequest.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizationRequest.java @@ -71,6 +71,8 @@ public class AuthorizationRequest extends BaseRequest { private String codeChallenge; private String codeChallengeMethod; + private String authorizationDetails; + private String dpopJkt; private Map customResponseHeaders; @@ -144,6 +146,24 @@ public void setDpopJkt(String dpopJkt) { this.dpopJkt = dpopJkt; } + /** + * Gets authorization details + * + * @return authorization details + */ + public String getAuthorizationDetails() { + return authorizationDetails; + } + + /** + * Authorization details + * + * @param authorizationDetails authorization details + */ + public void setAuthorizationDetails(String authorizationDetails) { + this.authorizationDetails = authorizationDetails; + } + /** * Returns the response types. * @@ -601,6 +621,7 @@ public String getQueryString() { addQueryStringParam(queryStringBuilder, AuthorizeRequestParam.CODE_CHALLENGE, codeChallenge); addQueryStringParam(queryStringBuilder, AuthorizeRequestParam.CODE_CHALLENGE_METHOD, codeChallengeMethod); addQueryStringParam(queryStringBuilder, AuthorizeRequestParam.DPOP_JKT, dpopJkt); + addQueryStringParam(queryStringBuilder, AuthorizeRequestParam.AUTHORIZATION_DETAILS, authorizationDetails); addQueryStringParam(queryStringBuilder, AuthorizeRequestParam.CUSTOM_RESPONSE_HEADERS, customResponseHeadersAsString); for (String key : getCustomParameters().keySet()) { @@ -662,6 +683,7 @@ public Map getParameters() { putNotBlank(parameters, AuthorizeRequestParam.CODE_CHALLENGE, codeChallenge); putNotBlank(parameters, AuthorizeRequestParam.CODE_CHALLENGE_METHOD, codeChallengeMethod); putNotBlank(parameters, AuthorizeRequestParam.DPOP_JKT, dpopJkt); + putNotBlank(parameters, AuthorizeRequestParam.AUTHORIZATION_DETAILS, authorizationDetails); putNotBlank(parameters, AuthorizeRequestParam.CUSTOM_RESPONSE_HEADERS, customResponseHeadersAsString); for (String key : getCustomParameters().keySet()) { diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizeClient.java b/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizeClient.java index 26b022b8fc2..3b7ce190b15 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizeClient.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizeClient.java @@ -226,6 +226,7 @@ private AuthorizationResponse exec_() throws Exception { addReqParam(AuthorizeRequestParam.UI_LOCALES, uiLocalesAsString); addReqParam(AuthorizeRequestParam.CLAIMS_LOCALES, claimLocalesAsString); addReqParam(AuthorizeRequestParam.ID_TOKEN_HINT, getRequest().getIdTokenHint()); + addReqParam(AuthorizeRequestParam.AUTHORIZATION_DETAILS, getRequest().getAuthorizationDetails()); addReqParam(AuthorizeRequestParam.LOGIN_HINT, getRequest().getLoginHint()); addReqParam(AuthorizeRequestParam.ACR_VALUES, acrValuesAsString); addReqParam(AuthorizeRequestParam.CLAIMS, claimsAsString); 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 4e6e0b55287..870e8f1ca35 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 @@ -135,6 +135,7 @@ public static void parse(String json, OpenIdConfigurationResponse response) { Util.addToListIfHas(response.getResponseModesSupported(), jsonObj, RESPONSE_MODES_SUPPORTED); Util.addToListIfHas(response.getGrantTypesSupported(), jsonObj, GRANT_TYPES_SUPPORTED); Util.addToListIfHas(response.getAcrValuesSupported(), jsonObj, ACR_VALUES_SUPPORTED); + Util.addToListIfHas(response.getAuthorizationDetailsTypesSupported(), jsonObj, AUTHORIZATION_DETAILS_TYPES_SUPPORTED); Util.addToListIfHas(response.getSubjectTypesSupported(), jsonObj, SUBJECT_TYPES_SUPPORTED); Util.addToListIfHas(response.getAuthorizationSigningAlgValuesSupported(), jsonObj, AUTHORIZATION_SIGNING_ALG_VALUES_SUPPORTED); Util.addToListIfHas(response.getAuthorizationEncryptionAlgValuesSupported(), jsonObj, AUTHORIZATION_ENCRYPTION_ALG_VALUES_SUPPORTED); 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 a0ebcdc3033..4c460ec5194 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 @@ -47,6 +47,7 @@ public class OpenIdConfigurationResponse extends BaseResponse implements Seriali private List responseModesSupported; private List grantTypesSupported; private List acrValuesSupported; + private List authorizationDetailsTypesSupported; private List subjectTypesSupported; private List authorizationSigningAlgValuesSupported; private List authorizationEncryptionAlgValuesSupported; @@ -110,6 +111,7 @@ public OpenIdConfigurationResponse(int status) { responseModesSupported = new ArrayList<>(); grantTypesSupported = new ArrayList<>(); acrValuesSupported = new ArrayList<>(); + authorizationDetailsTypesSupported = new ArrayList<>(); subjectTypesSupported = new ArrayList<>(); authorizationSigningAlgValuesSupported = new ArrayList<>(); authorizationEncryptionAlgValuesSupported = new ArrayList<>(); @@ -523,6 +525,24 @@ public void setAcrValuesSupported(List acrValuesSupported) { this.acrValuesSupported = acrValuesSupported; } + /** + * Gets authorization details types supported. + * + * @return authorization details types supported. + */ + public List getAuthorizationDetailsTypesSupported() { + return authorizationDetailsTypesSupported; + } + + /** + * Sets authorization details types supported. + * + * @param authorizationDetailsTypesSupported authorization details types supported. + */ + public void setAuthorizationDetailsTypesSupported(List authorizationDetailsTypesSupported) { + this.authorizationDetailsTypesSupported = authorizationDetailsTypesSupported; + } + /** * Returns a list of the subject identifier types that this server supports. * Valid types include pairwise and public. @@ -1257,6 +1277,7 @@ public String toString() { ", responseModesSupported=" + responseModesSupported + ", grantTypesSupported=" + grantTypesSupported + ", acrValuesSupported=" + acrValuesSupported + + ", authorizationDetailsTypesSupported=" + authorizationDetailsTypesSupported + ", subjectTypesSupported=" + subjectTypesSupported + ", authorizationSigningAlgValuesSupported=" + authorizationSigningAlgValuesSupported + ", authorizationEncryptionAlgValuesSupported=" + authorizationEncryptionAlgValuesSupported + diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/RegisterRequest.java b/jans-auth-server/client/src/main/java/io/jans/as/client/RegisterRequest.java index ba440fc869a..5d7bdafc8e1 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/RegisterRequest.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/RegisterRequest.java @@ -66,6 +66,7 @@ public class RegisterRequest extends BaseRequest { private List grantTypes; private ApplicationType applicationType; private List contacts; + private List authorizationDetailsTypes; private final LocalizedString clientName; private final LocalizedString logoUri; private final LocalizedString clientUri; @@ -171,6 +172,7 @@ public RegisterRequest() { this.responseTypes = new ArrayList<>(); this.grantTypes = new ArrayList<>(); this.contacts = new ArrayList<>(); + this.authorizationDetailsTypes = new ArrayList<>(); this.defaultAcrValues = new ArrayList<>(); this.minimumAcrPriorityList = new ArrayList<>(); this.postLogoutRedirectUris = new ArrayList<>(); @@ -480,6 +482,24 @@ public void setContacts(List contacts) { this.contacts = contacts; } + /** + * Gets authorization details types. + * + * @return authorization details types + */ + public List getAuthorizationDetailsTypes() { + return authorizationDetailsTypes; + } + + /** + * Sets authorization details types + * + * @param authorizationDetailsTypes authorization details types + */ + public void setAuthorizationDetailsTypes(List authorizationDetailsTypes) { + this.authorizationDetailsTypes = authorizationDetailsTypes; + } + /** * Returns the name of the Client to be presented to the user. * @@ -1760,6 +1780,7 @@ public static RegisterRequest fromJson(JSONObject requestObject) throws JSONExce result.setGrantTypes(extractGrantTypes(requestObject)); result.setApplicationType(ApplicationType.fromString(requestObject.optString(APPLICATION_TYPE.toString()))); result.setContacts(extractListByKey(requestObject, CONTACTS.toString())); + result.setAuthorizationDetailsTypes(extractListByKey(requestObject, AUTHORIZATION_DETAILS_TYPES.toString())); result.setIdTokenTokenBindingCnf(requestObject.optString(ID_TOKEN_TOKEN_BINDING_CNF.toString(), "")); LocalizedString.fromJson(requestObject, CLIENT_NAME.getName(), (String key, Locale locale) -> { @@ -1859,6 +1880,9 @@ public void getParameters(BiFunction function) { if (contacts != null && !contacts.isEmpty()) { function.apply(CONTACTS.toString(), toJSONArray(contacts)); } + if (authorizationDetailsTypes != null && !authorizationDetailsTypes.isEmpty()) { + function.apply(AUTHORIZATION_DETAILS_TYPES.toString(), toJSONArray(authorizationDetailsTypes)); + } if (StringUtils.isNotBlank(jwksUri)) { function.apply(JWKS_URI.toString(), jwksUri); diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/TokenClient.java b/jans-auth-server/client/src/main/java/io/jans/as/client/TokenClient.java index e0b75e507ea..d72e4c1504b 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/TokenClient.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/TokenClient.java @@ -229,6 +229,7 @@ public TokenResponse exec() { addFormParameterIfNotBlank(USERNAME, getRequest().getUsername()); addFormParameterIfNotBlank(PASSWORD, getRequest().getPassword()); addFormParameterIfNotBlank(SCOPE, getRequest().getScope()); + addFormParameterIfNotBlank(AUTHORIZATION_DETAILS, getRequest().getAuthorizationDetails()); addFormParameterIfNotBlank(ASSERTION, getRequest().getAssertion()); addFormParameterIfNotBlank(REFRESH_TOKEN, getRequest().getRefreshToken()); addFormParameterIfNotBlank(AUTH_REQ_ID, getRequest().getAuthReqId()); diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/TokenRequest.java b/jans-auth-server/client/src/main/java/io/jans/as/client/TokenRequest.java index 85bc1ab7c7d..85b1f87cafa 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/TokenRequest.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/TokenRequest.java @@ -33,6 +33,7 @@ public class TokenRequest extends ClientAuthnRequest { private String username; private String password; private String scope; + private String authorizationDetails; private String assertion; private String refreshToken; private String codeVerifier; @@ -234,6 +235,24 @@ public void setScope(String scope) { this.scope = scope; } + /** + * Gets authorization details + * + * @return authorization details + */ + public String getAuthorizationDetails() { + return authorizationDetails; + } + + /** + * Sets authorization details + * + * @param authorizationDetails authorization details + */ + public void setAuthorizationDetails(String authorizationDetails) { + this.authorizationDetails = authorizationDetails; + } + /** * Returns the assertion. * @@ -308,6 +327,7 @@ public String getQueryString() { builder.append("code", code); builder.append("redirect_uri", redirectUri); builder.append("scope", scope); + builder.append("authorizationDetails", authorizationDetails); builder.append("username", username); builder.append("password", password); builder.append("assertion", assertion); @@ -373,6 +393,9 @@ public Map getParameters() { if (scope != null && !scope.isEmpty()) { parameters.put("scope", scope); } + if (authorizationDetails != null && !authorizationDetails.isEmpty()) { + parameters.put("authorization_details", authorizationDetails); + } if (assertion != null && !assertion.isEmpty()) { parameters.put("assertion", assertion); } diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/par/ParClient.java b/jans-auth-server/client/src/main/java/io/jans/as/client/par/ParClient.java index 2673153685f..ddd57bc8fc5 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/par/ParClient.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/par/ParClient.java @@ -71,6 +71,7 @@ private ParResponse exec_() throws Exception { addReqParam(AuthorizeRequestParam.UI_LOCALES, uiLocalesAsString); addReqParam(AuthorizeRequestParam.CLAIMS_LOCALES, claimLocalesAsString); addReqParam(AuthorizeRequestParam.ID_TOKEN_HINT, getRequest().getAuthorizationRequest().getIdTokenHint()); + addReqParam(AuthorizeRequestParam.AUTHORIZATION_DETAILS, getRequest().getAuthorizationRequest().getAuthorizationDetails()); addReqParam(AuthorizeRequestParam.LOGIN_HINT, getRequest().getAuthorizationRequest().getLoginHint()); addReqParam(AuthorizeRequestParam.ACR_VALUES, acrValuesAsString); addReqParam(AuthorizeRequestParam.CLAIMS, claimsAsString); diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/AuthorizationDetailsHttpTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/AuthorizationDetailsHttpTest.java new file mode 100644 index 00000000000..597ca037759 --- /dev/null +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/AuthorizationDetailsHttpTest.java @@ -0,0 +1,154 @@ +package io.jans.as.client.ws.rs; + +import com.google.common.collect.Lists; +import io.jans.as.client.*; +import io.jans.as.client.client.AssertBuilder; +import io.jans.as.client.service.ClientFactory; +import io.jans.as.client.service.IntrospectionService; +import io.jans.as.model.authzdetails.AuthzDetails; +import io.jans.as.model.common.*; +import io.jans.as.model.register.ApplicationType; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.testng.Assert.assertTrue; +import static org.testng.AssertJUnit.assertNotNull; + +/** + * Authorization Details Http Tests consists of: + * 1. register client + * 2. send authorization request + * 3. login&authorize with Authorization Code Flow + * 4. get token at Token Endpoint and check response has authorization_details + * 5. Request user info + * 6. introspect access_token and check authorization_details are present + * + * "demo_authz_detail" is authorization details type which corresponds to demo AuthzDetail custom script. + * + * @author Yuriy Z + */ +public class AuthorizationDetailsHttpTest extends BaseTest { + + private static final String AUTHORIZATION_DETAILS = "[\n" + + " {\n" + + " \"type\": \"demo_authz_detail\",\n" + + " \"actions\": [\n" + + " \"list_accounts\",\n" + + " \"read_balances\"\n" + + " ],\n" + + " \"locations\": [\n" + + " \"https://example.com/accounts\"\n" + + " ],\n" + + " \"ui_representation\": \"Read balances and list accounts at https://example.com/accounts\"\n" + + " }\n" + + "]"; + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri"}) + @Test + public void authorizationWithAuthorizationDetails( + final String userId, final String userSecret, final String redirectUris, final String redirectUri) throws Exception { + showTitle("authorizationCodeFlow"); + + List responseTypes = Collections.singletonList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + + // 3. login&authorize with Authorization Code Flow + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + AssertBuilder.authorizationResponse(authorizationResponse) + .check(); + + // 4. Request access token using the authorization code and check response has authorization_details + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + tokenRequest.setAuthorizationDetails(AUTHORIZATION_DETAILS); + + TokenClient tokenClient1 = newTokenClient(tokenRequest); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + AssertBuilder.tokenResponse(tokenResponse1) + .check(); + + final String accessToken = tokenResponse1.getAccessToken(); + + // 5. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setExecutor(clientEngine(true)); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + AssertBuilder.userInfoResponse(userInfoResponse) + .check(); + + // 6. introspect access_token and check authorization_details are present + final IntrospectionService introspectionService = ClientFactory.instance().createIntrospectionService(introspectionEndpoint); + final IntrospectionResponse introspectionResponse = introspectionService.introspectToken("Bearer " + accessToken, accessToken); + + assertNotNull(introspectionResponse); + assertTrue(introspectionResponse.isActive()); + assertNotNull(introspectionResponse.getAuthorizationDetails()); + assertTrue(AuthzDetails.of(introspectionResponse.getAuthorizationDetails().toString()).similar(AUTHORIZATION_DETAILS)); + + System.out.println("Introspection response for access_token: " + accessToken); + System.out.println(introspectionResponse); + } + + public RegisterResponse registerClient(final String redirectUris, List responseTypes, List scopes) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "jans test app", + io.jans.as.model.util.StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(List.of(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT)); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PUBLIC); + registerRequest.setAuthorizationDetailsTypes(Lists.newArrayList("demo_authz_detail")); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + AssertBuilder.registerResponse(registerResponse).created().check(); + return registerResponse; + } + + private AuthorizationResponse requestAuthorization(final String userId, final String userSecret, final String redirectUri, + List responseTypes, List scopes, String clientId, String nonce) { + + + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthorizationDetails(AUTHORIZATION_DETAILS); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + AssertBuilder.authorizationResponse(authorizationResponse).check(); + return authorizationResponse; + } + +} diff --git a/jans-auth-server/client/src/test/resources/testng.xml b/jans-auth-server/client/src/test/resources/testng.xml index bc405f9501f..bd29ed3ad70 100644 --- a/jans-auth-server/client/src/test/resources/testng.xml +++ b/jans-auth-server/client/src/test/resources/testng.xml @@ -6,6 +6,11 @@ + + + + + diff --git a/jans-auth-server/docs/swagger.yaml b/jans-auth-server/docs/swagger.yaml index 407a50b8b99..3a54b51ff6b 100644 --- a/jans-auth-server/docs/swagger.yaml +++ b/jans-auth-server/docs/swagger.yaml @@ -262,6 +262,12 @@ paths: description: The JSON Web Key (JWK) Thumbprint [RFC7638] of the proof-of-possession public key using the SHA-256 hash function schema: type: string + - name: authorization_details + in: query + required: false + description: The request parameter authorization_details contains, in JSON notation, an array of objects. Each JSON object contains the data to specify the authorization requirements for a certain type of resource. The type of resource or access requirement is determined by the type field. + schema: + type: string responses: 200: description: OK @@ -291,6 +297,7 @@ paths: - server_error - temporarily_unavailable - invalid_request_redirect_uri + - invalid_authorization_details - login_required - session_selection_required - consent_required @@ -406,6 +413,9 @@ paths: dpop_jkt: type: string description: The JSON Web Key (JWK) Thumbprint [RFC7638] of the proof-of-possession public key using the SHA-256 hash function + authorization_details: + type: string + description: The request parameter authorization_details contains, in JSON notation, an array of objects. Each JSON object contains the data to specify the authorization requirements for a certain type of resource. The type of resource or access requirement is determined by the type field. responses: 200: @@ -436,6 +446,7 @@ paths: - server_error - temporarily_unavailable - invalid_request_redirect_uri + - invalid_authorization_details - login_required - session_selection_required - consent_required @@ -923,7 +934,6 @@ paths: title: GluuConfigurationResponse description: Client GluuAttribute by Dn(Distinguished Name) based on Authorization Scope. required: - - id_generation_endpoint - introspection_endpoint type: object properties: @@ -941,6 +951,11 @@ paths: additionalProperties: type: string description: Scope map object + authorization_details_types_supported: + type: array + description: Array of string. Each string represents supported type of authorization details. + items: + type: string 500: $ref: '#/components/responses/InternalServerError' /introspection: @@ -1060,6 +1075,7 @@ paths: - server_error - temporarily_unavailable - invalid_request_redirect_uri + - invalid_authorization_details - login_required - session_selection_required - consent_required @@ -1186,6 +1202,7 @@ paths: - server_error - temporarily_unavailable - invalid_request_redirect_uri + - invalid_authorization_details - login_required - session_selection_required - consent_required @@ -1322,6 +1339,11 @@ paths: client_name: type: string description: Name of the Client to be presented to the user. + authorization_details_types: + type: array + description: authorization details types (RFC9396). Fine-graned access. + items: + type: string logo_uri: type: string description: URL that references a logo for the Client application @@ -1742,6 +1764,11 @@ paths: description: e-mail addresses of people responsible for this Client. items: type: string + authorization_details_types: + type: array + description: authorization details types (RFC9396). Fine-graned access. + items: + type: string client_name: type: string description: Name of the Client to be presented to the user. @@ -2138,6 +2165,11 @@ paths: description: e-mail addresses of people responsible for this Client. items: type: string + authorization_details_types: + type: array + description: authorization details types (RFC9396). Fine-graned access. + items: + type: string client_name: type: string description: Name of the Client to be presented to the user. diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizeErrorResponseType.java b/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizeErrorResponseType.java index 83aed1fa13c..12718d0afd8 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizeErrorResponseType.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizeErrorResponseType.java @@ -72,6 +72,18 @@ public enum AuthorizeErrorResponseType implements IErrorType { */ INVALID_REQUEST_REDIRECT_URI("invalid_request_redirect_uri"), + /** + * invalid_authorization_details is returned to the client if any of the + * following are true of the objects in the authorization_details structure: + * + * - contains an unknown authorization details type value, + * - is an object of known type but containing unknown fields, + * - contains fields of the wrong type for the authorization details type, + * - contains fields with invalid values for the authorization details type, or + * - is missing required fields for the authorization details type. + */ + INVALID_AUTHORIZATION_DETAILS("invalid_authorization_details "), + /** * The Authorization Server requires End-User authentication. This error MAY * be returned when the prompt parameter in the Authorization Request is set diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizeRequestParam.java b/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizeRequestParam.java index 39f1319c306..83f086ced46 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizeRequestParam.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizeRequestParam.java @@ -32,6 +32,7 @@ public final class AuthorizeRequestParam { public static final String ACR_VALUES = "acr_values"; public static final String AMR_VALUES = "amr_values"; public static final String CLAIMS = "claims"; + public static final String AUTHORIZATION_DETAILS = "authorization_details"; public static final String REGISTRATION = "registration"; public static final String REQUEST = "request"; public static final String REQUEST_URI = "request_uri"; diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/authzdetails/AuthzDetail.java b/jans-auth-server/model/src/main/java/io/jans/as/model/authzdetails/AuthzDetail.java new file mode 100644 index 00000000000..08912eff13c --- /dev/null +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/authzdetails/AuthzDetail.java @@ -0,0 +1,44 @@ +package io.jans.as.model.authzdetails; + +import org.json.JSONObject; + +/** + * @author Yuriy Z + */ +public class AuthzDetail { + + private final JSONObject jsonObject; + private String uiRepresentation; + + public AuthzDetail(String json) { + this(new JSONObject(json)); + } + + public AuthzDetail(JSONObject jsonObject) { + this.jsonObject = jsonObject; + } + + public JSONObject getJsonObject() { + return jsonObject; + } + + public String getType() { + return jsonObject.optString("type"); + } + + public String getUiRepresentation() { + return uiRepresentation; + } + + public void setUiRepresentation(String uiRepresentation) { + this.uiRepresentation = uiRepresentation; + } + + @Override + public String toString() { + return "AuthzDetail{" + + "jsonObject=" + jsonObject + + "uiRepresentation=" + uiRepresentation + + '}'; + } +} diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/authzdetails/AuthzDetails.java b/jans-auth-server/model/src/main/java/io/jans/as/model/authzdetails/AuthzDetails.java new file mode 100644 index 00000000000..19d4c9740b8 --- /dev/null +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/authzdetails/AuthzDetails.java @@ -0,0 +1,105 @@ +package io.jans.as.model.authzdetails; + +import org.apache.commons.lang3.StringUtils; +import org.json.JSONArray; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Yuriy Z + */ +public class AuthzDetails { + + private final List details; + + public AuthzDetails(List details) { + this.details = details; + } + + public AuthzDetails() { + this(new ArrayList<>()); + } + + public static AuthzDetails of(String jsonArray) { + return of(new JSONArray(jsonArray)); + } + + public static AuthzDetails ofSilently(String jsonArray) { + try { + return of(new JSONArray(jsonArray)); + } catch (Exception e) { + return null; + } + } + + public static AuthzDetails of(JSONArray jsonArray) { + AuthzDetails result = new AuthzDetails(); + for (int i = 0; i < jsonArray.length(); i++) { + result.details.add(new AuthzDetail(jsonArray.getJSONObject(i))); + } + return result; + } + + public static boolean similar(String authorizationDetails1, String authorizationDetails2) { + if (StringUtils.equals(authorizationDetails1, authorizationDetails2)) { + return true; + } + if (authorizationDetails1 == null || authorizationDetails2 == null) { + return false; + } + JSONArray array1 = new JSONArray(authorizationDetails1); + JSONArray array2 = new JSONArray(authorizationDetails2); + return array1.similar(array2); + } + + public static String simpleMerge(String authorizationDetails1, String authorizationDetails2) { + final AuthzDetails details1 = AuthzDetails.of(authorizationDetails1); + final AuthzDetails details2 = AuthzDetails.of(authorizationDetails2); + details1.getDetails().addAll(details2.getDetails()); + return details1.asJsonArray().toString(); + } + + public JSONArray asJsonArray() { + JSONArray array = new JSONArray(); + array.putAll(details.stream().map(AuthzDetail::getJsonObject).collect(Collectors.toList())); + return array; + } + + public String asJsonString() { + return asJsonArray().toString(); + } + + public boolean similar(String authorizationDetails) { + if (StringUtils.isBlank(authorizationDetails)) { + return false; + } + return asJsonArray().similar(new JSONArray(authorizationDetails)); + } + + public List getDetails() { + return details; + } + + public Set getTypes() { + Set result = new HashSet<>(); + for (AuthzDetail d : details) { + result.add(d.getType()); + } + return result; + } + + public static boolean isEmpty(AuthzDetails authzDetails) { + return authzDetails == null || authzDetails.getDetails() == null || authzDetails.getDetails().isEmpty(); + } + + @Override + public String toString() { + return "AuthzDetails{" + + "details=" + details + + '}'; + } +} diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/common/IntrospectionResponse.java b/jans-auth-server/model/src/main/java/io/jans/as/model/common/IntrospectionResponse.java index 046dd2c7feb..c4a2e7368ee 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/common/IntrospectionResponse.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/common/IntrospectionResponse.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.jans.as.model.common.converter.ListConverter; import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; @@ -56,6 +57,8 @@ public class IntrospectionResponse { private String acr; @JsonProperty(value = "auth_time") private Integer authTime; + @JsonProperty(value = "authorization_details") + private JsonNode authorizationDetails; // DPoP @JsonProperty(value = "nbf") @@ -71,6 +74,14 @@ public IntrospectionResponse(boolean active) { this.active = active; } + public JsonNode getAuthorizationDetails() { + return authorizationDetails; + } + + public void setAuthorizationDetails(JsonNode authorizationDetails) { + this.authorizationDetails = authorizationDetails; + } + public String getAcr() { return acr; } @@ -196,6 +207,7 @@ public String toString() { return "IntrospectionResponse{" + "active=" + active + ", scope=" + scope + + ", authorizationDetails=" + authorizationDetails + ", clientId='" + clientId + '\'' + ", username='" + username + '\'' + ", tokenType='" + tokenType + '\'' + 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 0742f9a4e6d..059236c9836 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 @@ -39,6 +39,7 @@ private ConfigurationResponseClaim() { public static final String RESPONSE_MODES_SUPPORTED = "response_modes_supported"; public static final String GRANT_TYPES_SUPPORTED = "grant_types_supported"; public static final String ACR_VALUES_SUPPORTED = "acr_values_supported"; + public static final String AUTHORIZATION_DETAILS_TYPES_SUPPORTED = "authorization_details_types_supported"; public static final String SUBJECT_TYPES_SUPPORTED = "subject_types_supported"; public static final String AUTHORIZATION_SIGNING_ALG_VALUES_SUPPORTED = "authorization_signing_alg_values_supported"; public static final String AUTHORIZATION_ENCRYPTION_ALG_VALUES_SUPPORTED = "authorization_encryption_alg_values_supported"; diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterRequestParam.java b/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterRequestParam.java index 7f1d64cf0f0..a26742dc2a2 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterRequestParam.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterRequestParam.java @@ -67,6 +67,11 @@ public enum RegisterRequestParam { */ CONTACTS("contacts"), + /** + * Authorization Details Types (RFC9396). Fine-grained access. + */ + AUTHORIZATION_DETAILS_TYPES("authorization_details_types"), + /** * Name of the Client to be presented to the user. */ diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/token/TokenErrorResponseType.java b/jans-auth-server/model/src/main/java/io/jans/as/model/token/TokenErrorResponseType.java index fd143d17e3f..9dd53f714f7 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/token/TokenErrorResponseType.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/token/TokenErrorResponseType.java @@ -59,6 +59,18 @@ public enum TokenErrorResponseType implements IErrorType { */ INVALID_SCOPE("invalid_scope"), + /** + * invalid_authorization_details is returned to the client if any of the + * following are true of the objects in the authorization_details structure: + * + * - contains an unknown authorization details type value, + * - is an object of known type but containing unknown fields, + * - contains fields of the wrong type for the authorization details type, + * - contains fields with invalid values for the authorization details type, or + * - is missing required fields for the authorization details type. + */ + INVALID_AUTHORIZATION_DETAILS("invalid_authorization_details "), + /** * CIBA. The authorization request is still pending as the end-user hasn't yet been authenticated. */ diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/token/TokenRequestParam.java b/jans-auth-server/model/src/main/java/io/jans/as/model/token/TokenRequestParam.java index 8b660164585..05743776b3c 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/token/TokenRequestParam.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/token/TokenRequestParam.java @@ -23,6 +23,7 @@ private TokenRequestParam() { public static final String USERNAME = "username"; public static final String PASSWORD = "password"; public static final String SCOPE = "scope"; + public static final String AUTHORIZATION_DETAILS = "authorization_details"; public static final String ASSERTION = "assertion"; public static final String REFRESH_TOKEN = "refresh_token"; public static final String AUTH_REQ_ID = "auth_req_id"; diff --git a/jans-auth-server/model/src/test/java/io/jans/as/model/common/AuthzDetailsTest.java b/jans-auth-server/model/src/test/java/io/jans/as/model/common/AuthzDetailsTest.java new file mode 100644 index 00000000000..7bcb5275e02 --- /dev/null +++ b/jans-auth-server/model/src/test/java/io/jans/as/model/common/AuthzDetailsTest.java @@ -0,0 +1,100 @@ +package io.jans.as.model.common; + +import io.jans.as.model.authzdetails.AuthzDetail; +import io.jans.as.model.authzdetails.AuthzDetails; +import org.json.JSONArray; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.HashSet; + +import static org.testng.Assert.*; +import static org.testng.AssertJUnit.assertNotNull; + +/** + * @author Yuriy Z + */ +public class AuthzDetailsTest { + + @Test + public void isEmpty_forNull_shouldReturnTrue() { + assertTrue(AuthzDetails.isEmpty(null)); + } + + @Test + public void isEmpty_forEmptyDetails_shouldReturnTrue() { + assertTrue(AuthzDetails.isEmpty(new AuthzDetails())); + } + + @Test + public void isEmpty_forNonEmptyDetails_shouldReturnFalse() { + final AuthzDetails authzDetails = new AuthzDetails(); + authzDetails.getDetails().add(new AuthzDetail("{}")); + assertFalse(AuthzDetails.isEmpty(authzDetails)); + } + + @Test + public void ofSilently_withInvalidJson_shouldReturnNull() { + assertNull(AuthzDetails.ofSilently("invalidJson")); + } + + @Test + public void ofSilently_withValidJson_shouldReturnNotNull() { + assertNotNull(AuthzDetails.ofSilently("[]")); + } + + @Test + public void getTypes_withValidJson_shouldReturnNotNull() { + final AuthzDetails details = AuthzDetails.ofSilently("[{\"type\":\"internal_type\"}]"); + assertNotNull(details); + assertEquals(details.getTypes(), new HashSet<>(Collections.singletonList("internal_type"))); + } + + @Test + public void getJsonArray_withValidJson_shouldReturnNotNull() { + final AuthzDetails details = AuthzDetails.ofSilently("[{\"type\":\"internal_type\"}]"); + assertNotNull(details); + + final JSONArray array = details.asJsonArray(); + assertEquals(array.toString(), "[{\"type\":\"internal_type\"}]"); + assertTrue(array.similar(new JSONArray("[{\"type\":\"internal_type\"}]"))); + } + + @Test + public void similar_forSameJson_shouldReturnTrue() { + String a1 = "[\n" + + " {\n" + + " \"type\": \"internal_a1\"\n" + + " },\n" + + " {\n" + + " \"type\": \"internal_a2\"\n" + + " }\n" + + "]"; + + String a2 = "[\n" + + " {\n" + + " \"type\": \"internal_a1\"\n" + + " },\n" + + " {\n" + + " \"type\": \"internal_a2\"\n" + + " }\n" + + "]"; + + assertTrue(AuthzDetails.similar(a1, a2)); + } + + @Test + public void asJsonArray_whenCalled_shouldReturnExpectedArray() { + String a1 = "[\n" + + " {\n" + + " \"type\": \"internal_a1\"\n" + + " },\n" + + " {\n" + + " \"type\": \"internal_a2\"\n" + + " }\n" + + "]"; + + final AuthzDetails authzDetails = AuthzDetails.of(a1); + assertTrue(authzDetails.asJsonArray().similar(new JSONArray(a1))); + } +} diff --git a/jans-auth-server/model/src/test/resources/testng.xml b/jans-auth-server/model/src/test/resources/testng.xml index ea2fe4074b3..6d400d1f15a 100644 --- a/jans-auth-server/model/src/test/resources/testng.xml +++ b/jans-auth-server/model/src/test/resources/testng.xml @@ -1,9 +1,10 @@ - + + diff --git a/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java index 1e003b230eb..5ad64ada877 100644 --- a/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java +++ b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java @@ -137,6 +137,18 @@ public class ClientAttributes implements Serializable { @JsonProperty("introspectionEncryptedResponseEnc") private String introspectionEncryptedResponseEnc; + @JsonProperty("authorizationDetailsTypes") + private List authorizationDetailsTypes; + + public List getAuthorizationDetailsTypes() { + if (authorizationDetailsTypes == null) authorizationDetailsTypes = new ArrayList<>(); + return authorizationDetailsTypes; + } + + public void setAuthorizationDetailsTypes(List authorizationDetailsTypes) { + this.authorizationDetailsTypes = authorizationDetailsTypes; + } + public String getIntrospectionSignedResponseAlg() { return introspectionSignedResponseAlg; } @@ -504,6 +516,7 @@ public String toString() { ", introspectionSignedResponseAlg=" + introspectionSignedResponseAlg + ", introspectionEncryptedResponseAlg=" + introspectionEncryptedResponseAlg + ", introspectionEncryptedResponseEnc=" + introspectionEncryptedResponseEnc + + ", authorizationDetailsTypes=" + authorizationDetailsTypes + '}'; } } diff --git a/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAuthorization.java b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAuthorization.java index dd4281e120f..b59f41858f7 100644 --- a/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAuthorization.java +++ b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAuthorization.java @@ -6,13 +6,10 @@ package io.jans.as.persistence.model; -import io.jans.orm.annotation.AttributeName; -import io.jans.orm.annotation.DN; -import io.jans.orm.annotation.DataEntry; -import io.jans.orm.annotation.Expiration; -import io.jans.orm.annotation.ObjectClass; +import io.jans.orm.annotation.*; import java.io.Serializable; +import java.util.Arrays; import java.util.Date; /** @@ -44,9 +41,24 @@ public class ClientAuthorization implements Serializable { @AttributeName(name = "del") private boolean deletable = true; + @AttributeName(name = "jansAttrs") + @JsonObject + private ClientAuthorizationAttributes attributes; + @Expiration private Integer ttl; + public ClientAuthorizationAttributes getAttributes() { + if (attributes == null) { + attributes = new ClientAuthorizationAttributes(); + } + return attributes; + } + + public void setAttributes(ClientAuthorizationAttributes attributes) { + this.attributes = attributes; + } + public Integer getTtl() { return ttl; } @@ -128,4 +140,19 @@ public int hashCode() { result = 31 * result + id.hashCode(); return result; } + + @Override + public String toString() { + return "ClientAuthorization{" + + "dn='" + dn + '\'' + + ", id='" + id + '\'' + + ", clientId='" + clientId + '\'' + + ", userId='" + userId + '\'' + + ", scopes=" + Arrays.toString(scopes) + + ", expirationDate=" + expirationDate + + ", deletable=" + deletable + + ", attributes=" + attributes + + ", ttl=" + ttl + + '}'; + } } diff --git a/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAuthorizationAttributes.java b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAuthorizationAttributes.java new file mode 100644 index 00000000000..0ebb8d27460 --- /dev/null +++ b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAuthorizationAttributes.java @@ -0,0 +1,24 @@ +package io.jans.as.persistence.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +/** + * @author Yuriy Z + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ClientAuthorizationAttributes implements Serializable { + + @JsonProperty("authorizationDetails") + private String authorizationDetails; + + public String getAuthorizationDetails() { + return authorizationDetails; + } + + public void setAuthorizationDetails(String authorizationDetails) { + this.authorizationDetails = authorizationDetails; + } +} diff --git a/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ParAttributes.java b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ParAttributes.java index b311178d883..48eaac1af45 100644 --- a/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ParAttributes.java +++ b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ParAttributes.java @@ -20,6 +20,8 @@ public class ParAttributes implements Serializable { @JsonProperty private String scope; @JsonProperty + private String authorizationDetails; + @JsonProperty private String responseType; @JsonProperty private String clientId; @@ -82,6 +84,14 @@ public void setScope(String scope) { this.scope = scope; } + public String getAuthorizationDetails() { + return authorizationDetails; + } + + public void setAuthorizationDetails(String authorizationDetails) { + this.authorizationDetails = authorizationDetails; + } + public String getResponseType() { return responseType; } @@ -279,6 +289,7 @@ public void setCustomParameters(Map customParameters) { public String toString() { return "ParAttributes{" + "scope='" + scope + '\'' + + ", authorizationDetails='" + authorizationDetails + '\'' + ", responseType='" + responseType + '\'' + ", clientId='" + clientId + '\'' + ", redirectUri='" + redirectUri + '\'' + diff --git a/jans-auth-server/server/conf/jans-errors.json b/jans-auth-server/server/conf/jans-errors.json index 1b2165e4c55..45fac00106e 100644 --- a/jans-auth-server/server/conf/jans-errors.json +++ b/jans-auth-server/server/conf/jans-errors.json @@ -50,6 +50,11 @@ "description":"The redirect_uri in the Authorization Request does not match any of the Client's pre-registered redirect_uris.", "uri":null }, + { + "id":"invalid_authorization_details", + "description":"The authorization_details in the Authorization Request does not pass AS validation.", + "uri":null + }, { "id":"login_required", "description":"The Authorization Server requires End-User authentication. This error MAY be returned when the prompt parameter in the Authorization Request is set to none to request that the Authorization Server should not display any user interfaces to the End-User, but the Authorization Request cannot be completed without displaying a user interface for user authentication.", @@ -178,6 +183,11 @@ "description":"The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client, or is otherwise malformed.", "uri":null }, + { + "id":"invalid_authorization_details", + "description":"The authorization_details in the request does not pass AS validation.", + "uri":null + }, { "id":"invalid_client", "description":"Client authentication failed (e.g. unknown client, no client authentication included, or unsupported authentication method). The authorization server MAY return an HTTP 401 (Unauthorized) status code to indicate which HTTP authentication schemes are supported. If the client attempted to authenticate via the Authorization request header field, the authorization server MUST respond with an HTTP 401 (Unauthorized) status code, and include the WWW-Authenticate response header field matching the authentication scheme used by the client.", diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeEndpoint.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeEndpoint.java index 240d8099410..6c1474a5fa2 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeEndpoint.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeEndpoint.java @@ -43,6 +43,7 @@ public Response requestAuthorizationPost( @FormParam("nonce") String nonce, @FormParam("code_challenge") String codeChallenge, @FormParam("code_challenge_method") String codeChallengeMethod, + @FormParam("authorization_details") String authorizationDetails, @Context HttpServletRequest httpRequest, @Context HttpServletResponse httpResponse) { @@ -61,6 +62,7 @@ public Response requestAuthorizationPost( authzRequest.setHttpResponse(httpResponse); authzRequest.setCodeChallenge(codeChallenge); authzRequest.setCodeChallengeMethod(codeChallengeMethod); + authzRequest.setAuthzDetailsString(authorizationDetails); return authorizationChallengeService.requestAuthorization(authzRequest); } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java index d7e22b451f8..560c20a46ad 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java @@ -131,6 +131,7 @@ public Response authorize(AuthzRequest authzRequest) throws IOException, TokenBi authorizationChallengeValidator.validateGrantType(client, state); authorizationChallengeValidator.validateAccess(client); Set scopes = scopeChecker.checkScopesPolicy(client, authzRequest.getScope()); + authorizeRestWebServiceValidator.validateAuthorizationDetails(authzRequest, client); final ExecutionContext executionContext = ExecutionContext.of(authzRequest); @@ -161,6 +162,7 @@ public Response authorize(AuthzRequest authzRequest) throws IOException, TokenBi authorizationGrant.setJwtAuthorizationRequest(authzRequest.getJwtRequest()); authorizationGrant.setTokenBindingHash(TokenBindingMessage.getTokenBindingIdHashFromTokenBindingMessage(tokenBindingHeader, client.getIdTokenTokenBindingCnf())); authorizationGrant.setScopes(scopes); + authorizationGrant.setAuthzDetails(authzRequest.getAuthzDetails()); authorizationGrant.setCodeChallenge(authzRequest.getCodeChallenge()); authorizationGrant.setCodeChallengeMethod(authzRequest.getCodeChallengeMethod()); authorizationGrant.setClaims(authzRequest.getClaims()); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java index 1dcba83a75a..19e7e8034c5 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java @@ -6,6 +6,8 @@ package io.jans.as.server.authorize.ws.rs; +import io.jans.as.model.authzdetails.AuthzDetail; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.common.model.common.User; import io.jans.as.common.model.registration.Client; import io.jans.as.common.model.session.SessionId; @@ -37,10 +39,7 @@ import io.jans.as.server.security.Identity; import io.jans.as.server.service.*; import io.jans.as.server.service.ciba.CibaRequestService; -import io.jans.as.server.service.external.ExternalAuthenticationService; -import io.jans.as.server.service.external.ExternalConsentGatheringService; -import io.jans.as.server.service.external.ExternalPostAuthnService; -import io.jans.as.server.service.external.ExternalSelectAccountService; +import io.jans.as.server.service.external.*; import io.jans.as.server.service.external.context.ExternalPostAuthnContext; import io.jans.jsf2.message.FacesMessages; import io.jans.jsf2.service.FacesService; @@ -176,6 +175,9 @@ public class AuthorizeAction { @Inject private AuthorizeRestWebServiceValidator authorizeRestWebServiceValidator; + @Inject + private ExternalAuthzDetailTypeService externalAuthzDetailTypeService; + // OAuth 2.0 request parameters private String scope; private String responseType; @@ -198,6 +200,7 @@ public class AuthorizeAction { private String requestUri; private String codeChallenge; private String codeChallengeMethod; + private String authorizationDetails; private String claims; // CIBA Request parameter @@ -207,6 +210,7 @@ public class AuthorizeAction { private String sessionId; private String allowedScope; + private AuthzDetails authzDetails; public void checkUiLocales() { List uiLocalesList = null; @@ -235,6 +239,8 @@ public void checkUiLocales() { public void checkPermissionGranted() { try { + log.trace("checkPermissionGranted clientId={}", clientId); + checkPermissionGrantedInternal(); } catch (Exception e) { log.error("Failed to perform checkPermissionGranted()", e); @@ -249,6 +255,11 @@ public void checkPermissionGrantedInternal() throws IOException { return; } + if (log.isTraceEnabled()) { + log.trace("checkPermissionGrantedInternal - scope: {}, client_id: {}, prompts: {}, responseType: {}, authorization_details: {}", + scope, clientId, prompt, responseType, authorizationDetails); + } + Client client = null; try { client = clientService.getClient(clientId); @@ -264,9 +275,18 @@ public void checkPermissionGrantedInternal() throws IOException { return; } + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setHttpRequest((HttpServletRequest) externalContext.getRequest()); + authzRequest.setHttpResponse((HttpServletResponse) externalContext.getResponse()); + authzRequest.setClient(client); + authzRequest.setAuthzDetailsString(authorizationDetails); + authzRequest.setScope(scope); + authzRequest.setPrompt(prompt); + // Fix the list of scopes in the authorization page. Jans Auth #739 Set grantedScopes = scopeChecker.checkScopesPolicy(client, scope); allowedScope = io.jans.as.model.util.StringUtils.implode(grantedScopes, " "); + authzDetails = prepareAuthzDetails(authorizationDetails, authzRequest); SessionId session = getSession(); List prompts = io.jans.as.model.common.Prompt.fromString(prompt, " "); @@ -411,10 +431,6 @@ public void checkPermissionGrantedInternal() throws IOException { return; } - AuthzRequest authzRequest = new AuthzRequest(); - authzRequest.setHttpRequest((HttpServletRequest) externalContext.getRequest()); - authzRequest.setHttpResponse((HttpServletResponse) externalContext.getResponse()); - authzRequest.setClient(client); authzRequest.setSessionId(sessionId); ExternalPostAuthnContext postAuthnContext = new ExternalPostAuthnContext(client, session, authzRequest, prompts); @@ -469,6 +485,26 @@ public void checkPermissionGrantedInternal() throws IOException { } } + private AuthzDetails prepareAuthzDetails(String authorizationDetails, AuthzRequest authzRequest) { + authzDetails = AuthzDetails.ofSilently(authorizationDetails); + + if (authzDetails == null || authzDetails.getDetails() == null || authzDetails.getDetails().isEmpty()) { + return null; + } + + authzRequest.setAuthzDetails(authzDetails); + + ExecutionContext executionContext = ExecutionContext.of(authzRequest); + executionContext.setAuthzDetails(authzDetails); + + for (AuthzDetail detail : authzDetails.getDetails()) { + final String uiRepresentation = externalAuthzDetailTypeService.externalGetUiRepresentation(executionContext, detail); + detail.setUiRepresentation(uiRepresentation); + } + + return authzDetails; + } + private String getSelectAccountPage(Client client) { ExecutionContext executionContext = ExecutionContext.of(externalContext); executionContext.setClient(client); @@ -904,6 +940,33 @@ public void setCodeChallengeMethod(String codeChallengeMethod) { this.codeChallengeMethod = codeChallengeMethod; } + /** + * Returns parsed authz details with ui representation (which is shown on authorize page). + * + * @return parsed authz details with ui representation (which is shown on authorize page). + */ + public List getAuthzDetails() { + return authzDetails != null ? authzDetails.getDetails() : Collections.emptyList(); + } + + /** + * Returns authorization details as string json. + * + * @return authorization details as string json + */ + public String getAuthorizationDetails() { + return authorizationDetails; + } + + /** + * Sets authorization details string json. + * + * @param authorizationDetails authorization details string json + */ + public void setAuthorizationDetails(String authorizationDetails) { + this.authorizationDetails = authorizationDetails; + } + public String getClaims() { return claims; } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebService.java index 421456ba6ce..516fe790638 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebService.java @@ -82,9 +82,12 @@ public interface AuthorizeRestWebService { * @param codeChallenge PKCE code challenge * @param codeChallengeMethod PKCE code challenge method * @param authReqId A unique identifier to identify the CIBA authentication request made by the Client. - * @param dpopJkt The value of the dpop_jkt authorization request parameter is the JSON Web Key (JWK) Thumbprint + * @param dpopJkt The value of the dpop_jkt authorization request parameter is the JSON Web Key (JWK) Thumbprint * of the proof-of-possession public key using the SHA-256 hash function - the same value * as used for the jkt confirmation method defined + * @param authorizationDetails The request parameter authorization_details contains, in JSON notation, an array of objects. + * Each JSON object contains the data to specify the authorization requirements for a certain + * type of resource. The type of resource or access requirement is determined by the type field. * @param httpRequest http request * @param securityContext An injectable interface that provides access to security * related information. @@ -163,6 +166,7 @@ Response requestAuthorizationGet( @QueryParam("claims") String claims, @QueryParam("auth_req_id") String authReqId, @QueryParam("dpop_jkt") String dpopJkt, + @QueryParam("authorization_details") String authorizationDetails, @Context HttpServletRequest httpRequest, @Context HttpServletResponse httpResponse, @Context SecurityContext securityContext); @@ -216,6 +220,9 @@ Response requestAuthorizationGet( * @param dpopJkt The value of the dpop_jkt authorization request parameter is the JSON Web Key (JWK) Thumbprint * of the proof-of-possession public key using the SHA-256 hash function - the same value * as used for the jkt confirmation method defined + * @param authorizationDetails The request parameter authorization_details contains, in JSON notation, an array of objects. + * Each JSON object contains the data to specify the authorization requirements for a certain + * type of resource. The type of resource or access requirement is determined by the type field. * @param httpRequest http request * @param securityContext An injectable interface that provides access to security * related information. @@ -274,7 +281,7 @@ Response requestAuthorizationPost( @FormParam("client_id") String clientId, @FormParam("redirect_uri") String redirectUri, @FormParam("state") String state, - @QueryParam("response_mode") String responseMode, + @FormParam("response_mode") String responseMode, @FormParam("nonce") String nonce, @FormParam("display") String display, @FormParam("prompt") String prompt, @@ -288,12 +295,13 @@ Response requestAuthorizationPost( @FormParam("request_uri") String requestUri, @FormParam("session_id") String sessionId, @FormParam("origin_headers") String originHeaders, - @QueryParam("code_challenge") String codeChallenge, - @QueryParam("code_challenge_method") String codeChallengeMethod, - @QueryParam(AuthorizeRequestParam.CUSTOM_RESPONSE_HEADERS) String customResponseHeaders, - @QueryParam("claims") String claims, - @QueryParam("auth_req_id") String authReqId, - @QueryParam("dpop_jkt") String dpopJkt, + @FormParam("code_challenge") String codeChallenge, + @FormParam("code_challenge_method") String codeChallengeMethod, + @FormParam(AuthorizeRequestParam.CUSTOM_RESPONSE_HEADERS) String customResponseHeaders, + @FormParam("claims") String claims, + @FormParam("auth_req_id") String authReqId, + @FormParam("dpop_jkt") String dpopJkt, + @FormParam("authorization_details") String authorizationDetails, @Context HttpServletRequest httpRequest, @Context HttpServletResponse httpResponse, @Context SecurityContext securityContext); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java index 15ed2dce69b..7040bd6ff11 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java @@ -7,6 +7,7 @@ package io.jans.as.server.authorize.ws.rs; import com.google.common.collect.Maps; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.common.model.common.User; import io.jans.as.common.model.registration.Client; import io.jans.as.common.model.session.SessionId; @@ -182,7 +183,7 @@ public Response requestAuthorizationGet( String loginHint, String acrValues, String amrValues, String request, String requestUri, String sessionId, String originHeaders, String codeChallenge, String codeChallengeMethod, String customResponseHeaders, String claims, String authReqId, - String dpopJkt, + String dpopJkt, String authorizationDetails, HttpServletRequest httpRequest, HttpServletResponse httpResponse, SecurityContext securityContext) { AuthzRequest authzRequest = new AuthzRequest(); @@ -203,6 +204,7 @@ public Response requestAuthorizationGet( authzRequest.setAcrValues(acrValues); authzRequest.setAmrValues(amrValues); authzRequest.setDpopJkt(dpopJkt); + authzRequest.setAuthzDetailsString(authorizationDetails); authzRequest.setRequest(request); authzRequest.setRequestUri(requestUri); authzRequest.setSessionId(sessionId); @@ -226,7 +228,7 @@ public Response requestAuthorizationPost( String loginHint, String acrValues, String amrValues, String request, String requestUri, String sessionId, String originHeaders, String codeChallenge, String codeChallengeMethod, String customResponseHeaders, String claims, String authReqId, - String dpopJkt, + String dpopJkt, String authorizationDetails, HttpServletRequest httpRequest, HttpServletResponse httpResponse, SecurityContext securityContext) { AuthzRequest authzRequest = new AuthzRequest(); @@ -249,6 +251,7 @@ public Response requestAuthorizationPost( authzRequest.setRequest(request); authzRequest.setRequestUri(requestUri); authzRequest.setDpopJkt(dpopJkt); + authzRequest.setAuthzDetailsString(authorizationDetails); authzRequest.setSessionId(sessionId); authzRequest.setOriginHeaders(originHeaders); authzRequest.setCodeChallenge(codeChallenge); @@ -333,6 +336,7 @@ private ResponseBuilder authorize(AuthzRequest authzRequest) throws AcrChangedEx authzRequestService.createRedirectUriResponse(authzRequest); authorizeRestWebServiceValidator.validateAcrs(authzRequest, client); + authorizeRestWebServiceValidator.validateAuthorizationDetails(authzRequest, client); Set scopes = scopeChecker.checkScopesPolicy(client, authzRequest.getScope()); @@ -396,6 +400,7 @@ private ResponseBuilder authorize(AuthzRequest authzRequest) throws AcrChangedEx authorizationGrant.setJwtAuthorizationRequest(authzRequest.getJwtRequest()); authorizationGrant.setTokenBindingHash(TokenBindingMessage.getTokenBindingIdHashFromTokenBindingMessage(tokenBindingHeader, client.getIdTokenTokenBindingCnf())); authorizationGrant.setScopes(scopes); + authorizationGrant.setAuthzDetails(authzRequest.getAuthzDetails()); authorizationGrant.setCodeChallenge(authzRequest.getCodeChallenge()); authorizationGrant.setCodeChallengeMethod(authzRequest.getCodeChallengeMethod()); authorizationGrant.setClaims(authzRequest.getClaims()); @@ -419,6 +424,7 @@ private ResponseBuilder authorize(AuthzRequest authzRequest) throws AcrChangedEx authorizationGrant.setNonce(authzRequest.getNonce()); authorizationGrant.setJwtAuthorizationRequest(authzRequest.getJwtRequest()); authorizationGrant.setScopes(scopes); + authorizationGrant.setAuthzDetails(authzRequest.getAuthzDetails()); authorizationGrant.setClaims(authzRequest.getClaims()); // Store acr_values @@ -445,6 +451,7 @@ private ResponseBuilder authorize(AuthzRequest authzRequest) throws AcrChangedEx authorizationGrant.setNonce(authzRequest.getNonce()); authorizationGrant.setJwtAuthorizationRequest(authzRequest.getJwtRequest()); authorizationGrant.setScopes(scopes); + authorizationGrant.setAuthzDetails(authzRequest.getAuthzDetails()); authorizationGrant.setClaims(authzRequest.getClaims()); // Store authentication acr values @@ -629,6 +636,12 @@ private Pair grantAccessOrFetchClientAuthorization throw new NoLogWebApplicationException(redirectToAuthorizationPage(authzRequest)); } + // when authorization_details are present we do not allow to use persisted client authorization + final AuthzDetails authzDetails = authzRequest.getAuthzDetails(); + if (authzDetails != null && authzDetails.getDetails() != null && !authzDetails.getDetails().isEmpty()) { + return new Pair<>(clientAuthorization, clientAuthorizationFetched); + } + // There is no need to present the consent page: // - If Client is a Trusted Client. // - If session already contains all scopes @@ -813,6 +826,8 @@ private void runCiba(AuthzRequest authzRequest, Client client) { executionContext.setClient(client); executionContext.setCertAsPem(authzRequest.getHttpRequest().getHeader("X-ClientCert")); executionContext.setScopes(StringUtils.isNotBlank(authzRequest.getScope()) ? new HashSet<>(Arrays.asList(authzRequest.getScope().split(" "))) : new HashSet<>()); + executionContext.setAuthzRequest(authzRequest); + executionContext.setAuthzDetails(authzRequest.getAuthzDetails()); AccessToken accessToken = cibaGrant.createAccessToken(executionContext); log.debug("Issuing access token: {}", accessToken.getCode()); @@ -947,6 +962,7 @@ private Response redirectTo(String pathToRedirect, AuthzRequest authzRequest) { redirect.addResponseParameterIfNotBlank(AuthorizeRequestParam.CODE_CHALLENGE_METHOD, authzRequest.getCodeChallengeMethod()); redirect.addResponseParameterIfNotBlank(AuthorizeRequestParam.SESSION_ID, authzRequest.getSessionId()); redirect.addResponseParameterIfNotBlank(AuthorizeRequestParam.CLAIMS, authzRequest.getClaims()); + redirect.addResponseParameterIfNotBlank(AuthorizeRequestParam.AUTHORIZATION_DETAILS, authzRequest.getAuthzDetailsString()); // CIBA param redirect.addResponseParameterIfNotBlank(AuthorizeRequestParam.AUTH_REQ_ID, authzRequest.getAuthReqId()); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceValidator.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceValidator.java index 25512f56768..365fc29d08d 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceValidator.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceValidator.java @@ -7,6 +7,7 @@ package io.jans.as.server.authorize.ws.rs; import com.google.common.base.Strings; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.common.model.registration.Client; import io.jans.as.common.model.session.SessionId; import io.jans.as.common.model.session.SessionIdState; @@ -26,6 +27,7 @@ import io.jans.as.server.model.exception.InvalidRedirectUrlException; import io.jans.as.server.security.Identity; import io.jans.as.server.service.*; +import io.jans.as.server.service.external.ExternalAuthzDetailTypeService; import io.jans.as.server.service.external.session.SessionEvent; import io.jans.as.server.service.external.session.SessionEventType; import io.jans.as.server.util.RedirectUtil; @@ -83,6 +85,9 @@ public class AuthorizeRestWebServiceValidator { @Inject private Identity identity; + @Inject + private ExternalAuthzDetailTypeService externalAuthzDetailTypeService; + public Client validateClient(String clientId, String state) { return validateClient(clientId, state, false); } @@ -215,7 +220,7 @@ public void validateRequestObject(JwtAuthorizationRequest jwtRequest, RedirectUr log.error("The Nested JWT signature algorithm is not valid."); throw redirectUriResponse.createWebException(AuthorizeErrorResponseType.INVALID_REQUEST_OBJECT); } - } + } String redirectUri = jwtRequest.getRedirectUri(); Client client = clientService.getClient(jwtRequest.getClientId()); if (redirectUri != null && redirectionUriService.validateRedirectionUri(client, redirectUri) == null) { @@ -378,7 +383,7 @@ public void throwInvalidJwtRequestExceptionAsJwtMode(RedirectUriResponse redirec throw new WebApplicationException( RedirectUtil.getRedirectResponseBuilder(redirectUriResponse.getRedirectUri(), httpRequest).build()); } - + public WebApplicationException createInvalidJwtRequestException(RedirectUriResponse redirectUriResponse, String reason) { if (appConfiguration.isFapi()) { log.debug(reason); // in FAPI case log reason but don't send it since it's `reason` is not known. @@ -468,4 +473,47 @@ public void validateNotWebView(HttpServletRequest httpRequest) { } } } + + public void validateAuthorizationDetails(AuthzRequest authzRequest, Client client) { + final String authorizationDetailsString = authzRequest.getAuthzDetailsString(); + if (StringUtils.isBlank(authorizationDetailsString)) { + return; // nothing to validate + } + + // 1. check whether authz details is valid json and can be parsed + final AuthzDetails authzDetails = AuthzDetails.ofSilently(authorizationDetailsString); + if (authzDetails == null) { + log.debug("Unable to parse 'authorization_details' {}", authorizationDetailsString); + throw authzRequest.getRedirectUriResponse().createWebException(AuthorizeErrorResponseType.INVALID_AUTHORIZATION_DETAILS, + "Unable to parse 'authorization_details'"); + } + authzRequest.setAuthzDetails(authzDetails); + + if (authzDetails.getDetails() == null || authzDetails.getDetails().isEmpty()) { + return; // nothing to validate + } + + final Set requestAuthzDetailsTypes = authzDetails.getTypes(); + + // 2. check whether authorization_details type is supported globally by AS + final Set supportedAuthzDetailsTypes = externalAuthzDetailTypeService.getSupportedAuthzDetailsTypes(); + if (!supportedAuthzDetailsTypes.containsAll(requestAuthzDetailsTypes)) { + log.debug("Not all authorization_details type are supported. Requested {}. AS supports: {}", requestAuthzDetailsTypes, supportedAuthzDetailsTypes); + + throw authzRequest.getRedirectUriResponse().createWebException(AuthorizeErrorResponseType.INVALID_AUTHORIZATION_DETAILS, + "Found not supported 'authorization_details' type."); + } + + // 3. check whether authorization_details type is supported by client + if (!client.getAttributes().getAuthorizationDetailsTypes().containsAll(requestAuthzDetailsTypes)) { + log.debug("Client does not support all authorization_details types' {}. Client supports {}", + requestAuthzDetailsTypes, client.getAttributes().getAuthorizationDetailsTypes()); + + throw authzRequest.getRedirectUriResponse().createWebException(AuthorizeErrorResponseType.UNAUTHORIZED_CLIENT, + "Client does not support authorization_details type'"); + } + + // 4. external script validation + externalAuthzDetailTypeService.externalValidateAuthzDetails(authzRequest); + } } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzDetailsService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzDetailsService.java new file mode 100644 index 00000000000..4a498aa01f1 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzDetailsService.java @@ -0,0 +1,115 @@ +package io.jans.as.server.authorize.ws.rs; + +import io.jans.as.model.authzdetails.AuthzDetail; +import io.jans.as.model.authzdetails.AuthzDetails; +import io.jans.as.common.model.registration.Client; +import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.model.error.IErrorType; +import io.jans.as.model.token.TokenErrorResponseType; +import io.jans.as.server.model.common.AuthorizationGrant; +import io.jans.as.server.model.common.ExecutionContext; +import io.jans.as.server.service.external.ExternalAuthzDetailTypeService; +import jakarta.ejb.Stateless; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * @author Yuriy Z + */ +@Stateless +@Named +public class AuthzDetailsService { + + @Inject + private Logger log; + + @Inject + private ExternalAuthzDetailTypeService externalAuthzDetailTypeService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + public AuthzDetails validateAuthorizationDetails(String authorizationDetailsString, ExecutionContext executionContext) { + if (StringUtils.isBlank(authorizationDetailsString)) { + return null; // nothing to validate + } + + // 1. check whether authz details is valid json and can be parsed + final AuthzDetails authzDetails = AuthzDetails.ofSilently(authorizationDetailsString); + if (authzDetails == null) { + log.debug("Unable to parse 'authorization_details' {}", authorizationDetailsString); + throw new WebApplicationException(error(400, TokenErrorResponseType.INVALID_AUTHORIZATION_DETAILS, + "Unable to parse 'authorization_details'").build()); + } + + if (authzDetails.getDetails() == null || authzDetails.getDetails().isEmpty()) { + return null; // nothing to validate + } + + final Set requestAuthzDetailsTypes = authzDetails.getTypes(); + + // 2. check whether authorization_details type is supported globally by AS + final Set supportedAuthzDetailsTypes = externalAuthzDetailTypeService.getSupportedAuthzDetailsTypes(); + if (!supportedAuthzDetailsTypes.containsAll(requestAuthzDetailsTypes)) { + log.debug("Not all authorization_details type are supported. Requested {}. AS supports: {}", requestAuthzDetailsTypes, supportedAuthzDetailsTypes); + + throw new WebApplicationException(error(400, TokenErrorResponseType.INVALID_AUTHORIZATION_DETAILS, + "Found not supported 'authorization_details' type.").build()); + } + + // 3. check whether authorization_details type is supported by client + final Client client = executionContext.getClient(); + if (!client.getAttributes().getAuthorizationDetailsTypes().containsAll(requestAuthzDetailsTypes)) { + log.debug("Client does not support all authorization_details types' {}. Client supports {}", + requestAuthzDetailsTypes, client.getAttributes().getAuthorizationDetailsTypes()); + + throw new WebApplicationException(error(400, TokenErrorResponseType.UNAUTHORIZED_CLIENT, + "Client does not support authorization_details type'").build()); + } + + // 4. external script validation + executionContext.setAuthzDetails(authzDetails); + externalAuthzDetailTypeService.externalValidateAuthzDetails(executionContext); + return authzDetails; + } + + public Response.ResponseBuilder error(int status, IErrorType type, String reason) { + return Response.status(status).type(MediaType.APPLICATION_JSON_TYPE).entity(errorResponseFactory.errorAsJson(type, reason)); + } + + public AuthzDetails checkAuthzDetails(AuthzDetails requestedAuthzDetails, final AuthzDetails authorizedDetails) { + if (AuthzDetails.isEmpty(authorizedDetails) || AuthzDetails.isEmpty(requestedAuthzDetails)) { + return null; + } + + List grantedDetails = new ArrayList<>(); + + for (AuthzDetail authorized : authorizedDetails.getDetails()) { + for (AuthzDetail requested : requestedAuthzDetails.getDetails()) { + if (authorized.getJsonObject().similar(requested.getJsonObject()) && !grantedDetails.contains(authorized)) { + grantedDetails.add(authorized); + break; + } + } + } + + return new AuthzDetails(grantedDetails); + } + + public AuthzDetails checkAuthzDetailsAndSave(AuthzDetails requestedAuthzDetails, AuthorizationGrant authorizationGrant) { + final AuthzDetails granted = checkAuthzDetails(requestedAuthzDetails, authorizationGrant.getAuthzDetails()); + authorizationGrant.setAuthzDetails(granted); + authorizationGrant.save(); + + return granted; + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequest.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequest.java index ba70168c10f..9934aa0d3ac 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequest.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequest.java @@ -1,6 +1,7 @@ package io.jans.as.server.authorize.ws.rs; import com.google.common.collect.Sets; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.common.model.registration.Client; import io.jans.as.common.model.session.DeviceSession; import io.jans.as.model.common.Prompt; @@ -27,7 +28,7 @@ */ public class AuthzRequest { - private final static Logger log = LoggerFactory.getLogger(AuthzRequest.class); + private static final Logger log = LoggerFactory.getLogger(AuthzRequest.class); private String scope; private String responseType; @@ -54,6 +55,8 @@ public class AuthzRequest { private String claims; private String authReqId; private String dpopJkt; + private String authzDetailsString; + private AuthzDetails authzDetails; private String httpMethod; private String deviceSession; private DeviceSession deviceSessionObject; @@ -68,6 +71,22 @@ public class AuthzRequest { private OAuth2AuditLog auditLog; private boolean promptFromJwt; + public String getAuthzDetailsString() { + return authzDetailsString; + } + + public void setAuthzDetailsString(String authzDetailsString) { + this.authzDetailsString = authzDetailsString; + } + + public AuthzDetails getAuthzDetails() { + return authzDetails; + } + + public void setAuthzDetails(AuthzDetails authzDetails) { + this.authzDetails = authzDetails; + } + public String getDpopJkt() { return dpopJkt; } @@ -435,6 +454,8 @@ public String toString() { ", amrValues='" + amrValues + '\'' + ", request='" + request + '\'' + ", requestUri='" + requestUri + '\'' + + ", authzDetailsString='" + authzDetailsString + '\'' + + ", authzDetails='" + authzDetails + '\'' + ", sessionId='" + sessionId + '\'' + ", originHeaders='" + originHeaders + '\'' + ", codeChallenge='" + codeChallenge + '\'' + diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java index f2e60b33136..f74f8e601f6 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java @@ -11,6 +11,7 @@ import io.jans.as.common.util.RedirectUri; import io.jans.as.model.authorize.AuthorizeErrorResponseType; import io.jans.as.model.authorize.AuthorizeResponseParam; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.model.common.GrantType; import io.jans.as.model.common.ResponseMode; import io.jans.as.model.common.ScopeConstants; @@ -185,6 +186,12 @@ public boolean processPar(AuthzRequest authzRequest) { authzRequest.setState(StringUtils.isNotBlank(par.getAttributes().getState()) ? par.getAttributes().getState() : ""); + final String authorizationDetails = par.getAttributes().getAuthorizationDetails(); + if (StringUtils.isNotBlank(authorizationDetails)) { + authzRequest.setAuthzDetailsString(authorizationDetails); + authzRequest.setAuthzDetails(AuthzDetails.ofSilently(authorizationDetails)); + } + if (StringUtils.isNotBlank(par.getAttributes().getDpopJkt())) authzRequest.setDpopJkt(par.getAttributes().getDpopJkt()); if (StringUtils.isNotBlank(par.getAttributes().getNonce())) diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/introspection/ws/rs/IntrospectionWebService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/introspection/ws/rs/IntrospectionWebService.java index 4294093dfc3..ce4b10124da 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/introspection/ws/rs/IntrospectionWebService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/introspection/ws/rs/IntrospectionWebService.java @@ -6,7 +6,11 @@ package io.jans.as.server.introspection.ws.rs; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Lists; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.common.service.AttributeService; import io.jans.as.model.authorize.AuthorizeErrorResponseType; import io.jans.as.model.common.IntrospectionResponse; @@ -58,6 +62,7 @@ public class IntrospectionWebService { private static final Pair EMPTY = new Pair<>(null, false); + private static final ObjectMapper OBJECT_MAPPER = ServerUtil.createJsonMapper(); @Inject private Logger log; @@ -157,7 +162,7 @@ private Response introspect(String authorization, String accept, String token, final AuthorizationGrant grantOfIntrospectionToken = authorizationGrantList.getAuthorizationGrantByAccessToken(token); - AbstractToken tokenToIntrospect = fillResponse(token, response, grantOfIntrospectionToken); + fillResponse(token, response, grantOfIntrospectionToken); JSONObject responseAsJsonObject = createResponseAsJsonObject(response, grantOfIntrospectionToken); ExternalIntrospectionContext context = new ExternalIntrospectionContext(authorizationGrant, httpRequest, httpResponse, appConfiguration, attributeService); @@ -222,6 +227,16 @@ private AbstractToken fillResponse(String token, IntrospectionResponse response, response.setAudience(grantOfIntrospectionToken.getClientId()); response.setAuthTime(ServerUtil.dateToSeconds(grantOfIntrospectionToken.getAuthenticationTime())); + final AuthzDetails authzDetails = grantOfIntrospectionToken.getAuthzDetails(); + if (!AuthzDetails.isEmpty(authzDetails)) { + try { + JsonNode authorizationDetailsNode = OBJECT_MAPPER.readTree(authzDetails.asJsonString()); + response.setAuthorizationDetails(authorizationDetailsNode); + } catch (JsonProcessingException e) { + log.error(String.format("Failed to convert authorization_details %s", authzDetails.asJsonString()), e); + } + } + if (tokenToIntrospect instanceof AccessToken) { AccessToken accessToken = (AccessToken) tokenToIntrospect; response.setTokenType(accessToken.getTokenType() != null ? accessToken.getTokenType().getName() : io.jans.as.model.common.TokenType.BEARER.getName()); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AbstractAuthorizationGrant.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AbstractAuthorizationGrant.java index ad455fea5f1..377207f7ec1 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AbstractAuthorizationGrant.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AbstractAuthorizationGrant.java @@ -6,6 +6,7 @@ package io.jans.as.server.model.common; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.common.model.common.User; import io.jans.as.common.model.registration.Client; import io.jans.as.model.common.TokenType; @@ -55,6 +56,7 @@ public abstract class AbstractAuthorizationGrant implements IAuthorizationGrant private AuthorizationGrantType authorizationGrantType; private Client client; private Set scopes; + private AuthzDetails authzDetails; private String grantId; private JwtAuthorizationRequest jwtAuthorizationRequest; @@ -440,6 +442,18 @@ public Set getScopes() { return scopes; } + public String getAuthzDetailsAsString() { + return authzDetails != null ? authzDetails.asJsonArray().toString() : null; + } + + public AuthzDetails getAuthzDetails() { + return authzDetails; + } + + public void setAuthzDetails(AuthzDetails authzDetails) { + this.authzDetails = authzDetails; + } + @Override public JwtAuthorizationRequest getJwtAuthorizationRequest() { return jwtAuthorizationRequest; @@ -525,6 +539,6 @@ public String toString() { + '\'' + ", sessionDn='" + sessionDn + '\'' + ", codeChallenge='" + codeChallenge + '\'' + ", codeChallengeMethod='" + codeChallengeMethod + '\'' + ", authenticationTime=" + authenticationTime + ", scopes=" + scopes + ", authorizationGrantType=" + authorizationGrantType + ", tokenBindingHash=" + tokenBindingHash - + ", x5ts256=" + x5ts256 + ", claims=" + claims + '}'; + + ", x5ts256=" + x5ts256 + ", claims=" + claims + ", authzDetails=" + authzDetails + '}'; } } \ No newline at end of file diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrant.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrant.java index 517460f40c2..b174e024875 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrant.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrant.java @@ -8,6 +8,7 @@ import com.google.common.collect.Lists; import io.jans.as.common.claims.Audience; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.common.model.common.User; import io.jans.as.common.model.registration.Client; import io.jans.as.common.service.AttributeService; @@ -174,6 +175,8 @@ private void initTokenFromGrant(TokenEntity token) { if (nonce != null) { token.setNonce(nonce); } + + token.getAttributes().setAuthorizationDetails(getAuthzDetailsAsString()); token.setScope(getScopesAsString()); token.setAuthMode(getAcrValues()); token.setSessionDn(getSessionDn()); @@ -265,6 +268,11 @@ public JwtSigner createAccessTokenAsJwt(AccessToken accessToken, ExecutionContex jwt.getClaims().setSubjectIdentifier(getSub()); jwt.getClaims().setClaim("x5t#S256", accessToken.getX5ts256()); + final AuthzDetails authzDetails = getAuthzDetails(); + if (!AuthzDetails.isEmpty(authzDetails)) { + jwt.getClaims().setClaim("authorization_details", authzDetails.asJsonArray()); + } + // DPoP final String dpop = context.getDpop(); if (StringUtils.isNotBlank(dpop)) { diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrantList.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrantList.java index 97c7cbfce3c..0a281eb6ccd 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrantList.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrantList.java @@ -6,6 +6,7 @@ package io.jans.as.server.model.common; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.common.model.common.User; import io.jans.as.common.model.registration.Client; import io.jans.as.common.service.common.UserService; @@ -340,6 +341,7 @@ public AuthorizationGrant asGrant(TokenEntity tokenEntity) { result.setGrantId(grantId); } result.setScopes(Util.splittedStringAsList(tokenEntity.getScope(), " ")); + result.setAuthzDetails(AuthzDetails.ofSilently(tokenEntity.getAttributes().getAuthorizationDetails())); result.setCodeChallenge(tokenEntity.getCodeChallenge()); result.setCodeChallengeMethod(tokenEntity.getCodeChallengeMethod()); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CIBAGrant.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CIBAGrant.java index e6c182e3d89..11f10c0886b 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CIBAGrant.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CIBAGrant.java @@ -6,6 +6,7 @@ package io.jans.as.server.model.common; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.model.common.GrantType; import io.jans.service.CacheService; @@ -38,6 +39,7 @@ public void init(CibaRequestCacheControl cibaRequest) { setAuthReqId(cibaRequest.getAuthReqId()); setAcrValues(cibaRequest.getAcrValues()); setScopes(cibaRequest.getScopes()); + setAuthzDetails(AuthzDetails.ofSilently(cibaRequest.getAuthzDetails())); setIsCachedWithNoPersistence(true); } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CacheGrant.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CacheGrant.java index 66696a51fcc..9934d86f372 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CacheGrant.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CacheGrant.java @@ -6,6 +6,7 @@ package io.jans.as.server.model.common; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.common.model.common.User; import io.jans.as.common.model.registration.Client; import io.jans.as.model.configuration.AppConfiguration; @@ -30,6 +31,7 @@ public class CacheGrant implements Serializable { private Client client; private Date authenticationTime; private Set scopes; + private String authzDetails; private String grantId; private String tokenBindingHash; private String nonce; @@ -62,6 +64,7 @@ public CacheGrant(AuthorizationGrant grant, AppConfiguration appConfiguration) { client = grant.getClient(); authenticationTime = grant.getAuthenticationTime(); scopes = grant.getScopes(); + authzDetails = grant.getAuthzDetailsAsString(); tokenBindingHash = grant.getTokenBindingHash(); grantId = grant.getGrantId(); nonce = grant.getNonce(); @@ -84,6 +87,7 @@ public CacheGrant(CIBAGrant grant, AppConfiguration appConfiguration) { client = grant.getClient(); authenticationTime = grant.getAuthenticationTime(); scopes = grant.getScopes(); + authzDetails = grant.getAuthzDetailsAsString(); tokenBindingHash = grant.getTokenBindingHash(); grantId = grant.getGrantId(); nonce = grant.getNonce(); @@ -109,6 +113,7 @@ public CacheGrant(DeviceCodeGrant grant, AppConfiguration appConfiguration) { client = grant.getClient(); authenticationTime = grant.getAuthenticationTime(); scopes = grant.getScopes(); + authzDetails = grant.getAuthzDetailsAsString(); tokenBindingHash = grant.getTokenBindingHash(); grantId = grant.getGrantId(); nonce = grant.getNonce(); @@ -148,6 +153,14 @@ public void setUser(User user) { this.user = user; } + public String getAuthzDetails() { + return authzDetails; + } + + public void setAuthzDetails(String authzDetails) { + this.authzDetails = authzDetails; + } + public Set getScopes() { return scopes; } @@ -242,6 +255,7 @@ public AuthorizationCodeGrant asCodeGrant(Instance g grant.setAuthorizationCode(new AuthorizationCode(authorizationCodeString, authorizationCodeCreationDate, authorizationCodeExpirationDate)); grant.setScopes(scopes); + grant.setAuthzDetails(AuthzDetails.ofSilently(authzDetails)); grant.setGrantId(grantId); grant.setSessionDn(sessionDn); grant.setCodeChallenge(codeChallenge); @@ -257,6 +271,7 @@ public CIBAGrant asCibaGrant(Instance grantInstance) CIBAGrant grant = grantInstance.select(CIBAGrant.class).get(); grant.init(user, AuthorizationGrantType.CIBA, client, authenticationTime); grant.setScopes(scopes); + grant.setAuthzDetails(AuthzDetails.ofSilently(authzDetails)); grant.setGrantId(grantId); grant.setSessionDn(sessionDn); grant.setCodeChallenge(codeChallenge); @@ -274,6 +289,7 @@ public DeviceCodeGrant asDeviceCodeGrant(Instance gr DeviceCodeGrant grant = grantInstance.select(DeviceCodeGrant.class).get(); grant.init(user, AuthorizationGrantType.DEVICE_CODE, client, authenticationTime); grant.setScopes(scopes); + grant.setAuthzDetails(AuthzDetails.ofSilently(getAuthzDetails())); grant.setGrantId(grantId); grant.setSessionDn(sessionDn); grant.setCodeChallenge(codeChallenge); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CibaRequestCacheControl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CibaRequestCacheControl.java index 80ab315a450..51202568bf9 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CibaRequestCacheControl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CibaRequestCacheControl.java @@ -25,6 +25,7 @@ public class CibaRequestCacheControl implements Serializable { private User user; private Client client; private List scopes; + private String authzDetails; private int expiresIn = 1; private String clientNotificationToken; @@ -145,6 +146,14 @@ public void setTokensDelivered(boolean tokensDelivered) { this.tokensDelivered = tokensDelivered; } + public String getAuthzDetails() { + return authzDetails; + } + + public void setAuthzDetails(String authzDetails) { + this.authzDetails = authzDetails; + } + public String getAcrValues() { return acrValues; } @@ -167,6 +176,7 @@ public String toString() { ", userAuthorization=" + status + ", tokensDelivered=" + tokensDelivered + ", acrValues='" + acrValues + '\'' + + ", authzDetails='" + authzDetails + '\'' + '}'; } } \ No newline at end of file diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/DeviceAuthorizationCacheControl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/DeviceAuthorizationCacheControl.java index 50b49957ac4..7db859aa2ac 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/DeviceAuthorizationCacheControl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/DeviceAuthorizationCacheControl.java @@ -21,6 +21,7 @@ public class DeviceAuthorizationCacheControl implements Serializable { private String deviceCode; private Client client; private List scopes; + private String authzDetails; private URI verificationUri; private int expiresIn = 1; private int interval = 5; @@ -76,6 +77,14 @@ public void setScopes(List scopes) { this.scopes = scopes; } + public String getAuthzDetails() { + return authzDetails; + } + + public void setAuthzDetails(String authzDetails) { + this.authzDetails = authzDetails; + } + public URI getVerificationUri() { return verificationUri; } @@ -128,6 +137,7 @@ public String toString() { ", interval=" + interval + ", lastAccessControl=" + lastAccessControl + ", status=" + status + + ", authzDetails=" + authzDetails + '}'; } } \ No newline at end of file diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/DeviceCodeGrant.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/DeviceCodeGrant.java index 12b33884fe1..0de34120520 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/DeviceCodeGrant.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/DeviceCodeGrant.java @@ -6,6 +6,7 @@ package io.jans.as.server.model.common; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.common.model.common.User; import io.jans.as.model.common.GrantType; import io.jans.service.CacheService; @@ -31,6 +32,7 @@ public void init(DeviceAuthorizationCacheControl cacheData, User user) { setDeviceCode(cacheData.getDeviceCode()); setIsCachedWithNoPersistence(true); setScopes(cacheData.getScopes()); + setAuthzDetails(AuthzDetails.ofSilently(cacheData.getAuthzDetails())); } @Override 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 0848918b145..a4c528e05f7 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 @@ -6,6 +6,8 @@ package io.jans.as.server.model.common; +import io.jans.as.model.authzdetails.AuthzDetail; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.common.model.common.User; import io.jans.as.common.model.registration.Client; import io.jans.as.common.model.session.SessionId; @@ -44,6 +46,8 @@ public class ExecutionContext { private List currentSessions; private AuthzRequest authzRequest; + private AuthzDetails authzDetails; + private AuthzDetail authzDetail; private AppConfiguration appConfiguration; private AttributeService attributeService; @@ -91,6 +95,7 @@ public static ExecutionContext of(AuthzRequest authzRequest) { executionContext.setHttpResponse(authzRequest.getHttpResponse()); executionContext.setClient(authzRequest.getClient()); executionContext.setAuthzRequest(authzRequest); + executionContext.setAuthzDetails(authzRequest.getAuthzDetails()); return executionContext; } @@ -107,6 +112,22 @@ public static ExecutionContext of(ExternalContext externalContext) { return executionContext; } + public AuthzDetails getAuthzDetails() { + return authzDetails; + } + + public void setAuthzDetails(AuthzDetails authzDetails) { + this.authzDetails = authzDetails; + } + + public AuthzDetail getAuthzDetail() { + return authzDetail; + } + + public void setAuthzDetail(AuthzDetail authzDetail) { + this.authzDetail = authzDetail; + } + public AuthzRequest getAuthzRequest() { return authzRequest; } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/ldap/TokenAttributes.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/ldap/TokenAttributes.java index 0af0c793376..bcbde39f8ed 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/ldap/TokenAttributes.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/ldap/TokenAttributes.java @@ -29,6 +29,16 @@ public class TokenAttributes implements Serializable { private Map attributes; @JsonProperty("dpopJkt") private String dpopJkt; + @JsonProperty("authorizationDetails") + private String authorizationDetails; + + public String getAuthorizationDetails() { + return authorizationDetails; + } + + public void setAuthorizationDetails(String authorizationDetails) { + this.authorizationDetails = authorizationDetails; + } public String getDpopJkt() { return dpopJkt; @@ -70,6 +80,7 @@ public String toString() { "x5cs256='" + x5cs256 + '\'' + "onlineAccess='" + onlineAccess + '\'' + "dpopJkt='" + dpopJkt + '\'' + + "authorizationDetails='" + authorizationDetails + '\'' + '}'; } } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/par/ws/rs/ParRestWebService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/par/ws/rs/ParRestWebService.java index 66d27df52e6..3add3c622fc 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/par/ws/rs/ParRestWebService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/par/ws/rs/ParRestWebService.java @@ -75,6 +75,7 @@ public class ParRestWebService { @Produces({MediaType.APPLICATION_JSON}) public Response requestPushedAuthorizationRequest( @FormParam("scope") String scope, + @FormParam("authorization_details") String authorizationDetails, @FormParam("response_type") String responseType, @FormParam("client_id") String clientId, @FormParam("redirect_uri") String redirectUri, @@ -121,8 +122,8 @@ public Response requestPushedAuthorizationRequest( log.debug("Attempting to request PAR: " + "acrValues = {}, amrValues = {}, originHeaders = {}, codeChallenge = {}, codeChallengeMethod = {}, " - + "customRespHeaders = {}, claims = {}, tokenBindingHeader = {}", - acrValuesStr, amrValuesStr, originHeaders, codeChallenge, codeChallengeMethod, customResponseHeaders, claims, tokenBindingHeader); + + "customRespHeaders = {}, claims = {}, tokenBindingHeader = {}, authorizationDetails = {}", + acrValuesStr, amrValuesStr, originHeaders, codeChallenge, codeChallengeMethod, customResponseHeaders, claims, tokenBindingHeader, authorizationDetails); List responseTypes = ResponseType.fromString(responseType, " "); ResponseMode responseModeObj = ResponseMode.getByValue(responseMode); @@ -148,6 +149,7 @@ public Response requestPushedAuthorizationRequest( par.setTtl(parLifetime); par.setExpirationDate(Util.createExpirationDate(parLifetime)); par.getAttributes().setScope(scope); + par.getAttributes().setAuthorizationDetails(authorizationDetails); par.getAttributes().setNbf(Util.parseIntegerSilently(nbf)); par.getAttributes().setResponseType(responseType); par.getAttributes().setClientId(clientId); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterJsonService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterJsonService.java index bb80547bd14..6b5ed7ad667 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterJsonService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterJsonService.java @@ -97,6 +97,7 @@ public JSONObject getJSONObject(Client client) throws JSONException, StringEncry Util.addToJSONObjectIfNotNull(responseJsonObject, GRANT_TYPES.toString(), GrantType.toStringArray(client.getGrantTypes())); Util.addToJSONObjectIfNotNull(responseJsonObject, APPLICATION_TYPE.toString(), client.getApplicationType()); Util.addToJSONObjectIfNotNull(responseJsonObject, CONTACTS.toString(), client.getContacts()); + Util.addToJSONObjectIfNotNull(responseJsonObject, AUTHORIZATION_DETAILS_TYPES.toString(), client.getAttributes().getAuthorizationDetailsTypes()); Util.addToJSONObjectIfNotNull(responseJsonObject, JWKS_URI.toString(), client.getJwksUri()); Util.addToJSONObjectIfNotNull(responseJsonObject, SECTOR_IDENTIFIER_URI.toString(), client.getSectorIdentifierUri()); Util.addToJSONObjectIfNotNull(responseJsonObject, SUBJECT_TYPE.toString(), client.getSubjectType()); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterService.java index cdc45cf71dc..5d0ce3b0e6b 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterService.java @@ -196,6 +196,11 @@ public void updateClientFromRequestObject(Client client, RegisterRequest request client.setContacts(listAsArrayWithoutDuplicates(contacts)); } + List authorizationDetailsTypes = requestObject.getAuthorizationDetailsTypes(); + if (authorizationDetailsTypes != null && !authorizationDetailsTypes.isEmpty()) { + client.getAttributes().setAuthorizationDetailsTypes(authorizationDetailsTypes); + } + for (String key : requestObject.getClientNameLanguageTags()) { client.setClientNameLocalized(requestObject.getClientName(key), Locale.forLanguageTag(key)); } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/AuthorizeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/AuthorizeService.java index 813d215b0a5..c5e8a3621c5 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/AuthorizeService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/AuthorizeService.java @@ -168,12 +168,13 @@ public void permissionGranted(HttpServletRequest httpRequest, final SessionId se } String scope = session.getSessionAttributes().get(AuthorizeRequestParam.SCOPE); + String authorizationDetails = session.getSessionAttributes().get(AuthorizeRequestParam.AUTHORIZATION_DETAILS); Set scopeSet = Sets.newHashSet(spaceSeparatedToList(scope)); String responseType = session.getSessionAttributes().get(AuthorizeRequestParam.RESPONSE_TYPE); boolean persistDuringImplicitFlow = !io.jans.as.model.common.ResponseType.isImplicitFlow(responseType); if (!client.getTrustedClient() && persistDuringImplicitFlow && client.getPersistClientAuthorizations()) { - clientAuthorizationsService.add(user.getAttribute("inum"), client.getClientId(), scopeSet); + clientAuthorizationsService.add(user.getAttribute("inum"), client.getClientId(), scopeSet, authorizationDetails); } session.addPermission(clientId, true, scopeSet); sessionIdService.updateSessionId(session); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/CleanerTimer.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/CleanerTimer.java index 3ea8b501e72..50de4c2ab1d 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/CleanerTimer.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/CleanerTimer.java @@ -37,6 +37,7 @@ import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import jakarta.inject.Named; +import org.apache.tika.utils.StringUtils; import org.slf4j.Logger; import java.util.Date; @@ -149,6 +150,11 @@ public void processImpl() { final Set processedBaseDns = new HashSet<>(); for (Map.Entry> baseDn : createCleanServiceBaseDns().entrySet()) { + if (StringUtils.isBlank(baseDn.getKey())) { + log.trace("BaseDN key is blank for class: {}", baseDn.getValue()); + continue; + } + final String processedKey = createProcessedKey(baseDn); if (entryManager.hasExpirationSupport(baseDn.getKey()) || processedBaseDns.contains(processedKey)) { continue; diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/ClientAuthorizationsService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/ClientAuthorizationsService.java index 9ffb2ecf5a5..9fa37d90009 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/ClientAuthorizationsService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/ClientAuthorizationsService.java @@ -6,6 +6,7 @@ package io.jans.as.server.service; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.common.model.registration.Client; import io.jans.as.model.config.StaticConfiguration; import io.jans.as.model.configuration.AppConfiguration; @@ -14,11 +15,12 @@ import io.jans.orm.exception.EntryPersistenceException; import io.jans.orm.model.base.SimpleBranch; import io.jans.util.StringHelper; +import jakarta.inject.Inject; +import jakarta.inject.Named; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; -import jakarta.inject.Inject; -import jakarta.inject.Named; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -94,8 +96,9 @@ public void clearAuthorizations(ClientAuthorization clientAuthorization, boolean } } - public void add(String userInum, String clientId, Set scopes) { - log.trace("Attempting to add client authorization, scopes:" + scopes + ", clientId: " + clientId + ", userInum: " + userInum); + public void add(String userInum, String clientId, Set scopes, String authorizationDetails) { + log.trace("Attempting to add client authorization, scopes: {}, clientId: {}, userInum: {}, authorizationDetails: {}", + scopes, clientId , userInum, authorizationDetails); Client client = clientService.getClient(clientId); @@ -116,6 +119,7 @@ public void add(String userInum, String clientId, Set scopes) { clientAuthorization.setClientId(clientId); clientAuthorization.setUserId(userInum); clientAuthorization.setScopes(scopes.toArray(new String[scopes.size()])); + clientAuthorization.getAttributes().setAuthorizationDetails(authorizationDetails); clientAuthorization.setDeletable(!client.getAttributes().getKeepClientAuthorizationAfterExpiration()); clientAuthorization.setExpirationDate(client.getExpirationDate()); clientAuthorization.setTtl(appConfiguration.getDynamicRegistrationExpirationTime()); @@ -125,8 +129,27 @@ public void add(String userInum, String clientId, Set scopes) { Set set = new HashSet<>(scopes); set.addAll(Arrays.asList(clientAuthorization.getScopes())); + boolean changed = false; + if (set.size() != clientAuthorization.getScopes().length) { clientAuthorization.setScopes(set.toArray(new String[set.size()])); + changed = true; + } + + if (StringUtils.isNotBlank(authorizationDetails)) { + if (StringUtils.isBlank(clientAuthorization.getAttributes().getAuthorizationDetails())) { + clientAuthorization.getAttributes().setAuthorizationDetails(authorizationDetails); + changed = true; + } else { + boolean isAuthorizationDetailsChanged = !AuthzDetails.similar(authorizationDetails, clientAuthorization.getAttributes().getAuthorizationDetails()); + if (isAuthorizationDetailsChanged) { + clientAuthorization.getAttributes().setAuthorizationDetails(AuthzDetails.simpleMerge(authorizationDetails, clientAuthorization.getAttributes().getAuthorizationDetails())); + changed = true; + } + } + } + + if (changed) { ldapEntryManager.merge(clientAuthorization); } } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/RequestParameterService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/RequestParameterService.java index 4ee8dce6d4d..fc8250160ed 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/RequestParameterService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/RequestParameterService.java @@ -56,6 +56,7 @@ public class RequestParameterService { AuthorizeRequestParam.MAX_AGE, AuthorizeRequestParam.UI_LOCALES, AuthorizeRequestParam.ID_TOKEN_HINT, + AuthorizeRequestParam.AUTHORIZATION_DETAILS, AuthorizeRequestParam.LOGIN_HINT, AuthorizeRequestParam.ACR_VALUES, AuthorizeRequestParam.REQUEST, diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthzDetailTypeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthzDetailTypeService.java new file mode 100644 index 00000000000..028c8741387 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthzDetailTypeService.java @@ -0,0 +1,133 @@ +package io.jans.as.server.service.external; + +import io.jans.as.model.authzdetails.AuthzDetail; +import io.jans.as.model.authzdetails.AuthzDetails; +import io.jans.as.model.authorize.AuthorizeErrorResponseType; +import io.jans.as.server.authorize.ws.rs.AuthzRequest; +import io.jans.as.server.model.common.ExecutionContext; +import io.jans.as.server.service.external.context.ExternalScriptContext; +import io.jans.model.custom.script.CustomScriptType; +import io.jans.model.custom.script.conf.CustomScriptConfiguration; +import io.jans.model.custom.script.type.authzdetails.AuthzDetailType; +import io.jans.service.custom.script.ExternalScriptService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.WebApplicationException; + +import java.util.Set; + +/** + * Authz Detail custom script service. It handles one single authz detail type (not many). + * + * @author Yuriy Z + */ +@ApplicationScoped +public class ExternalAuthzDetailTypeService extends ExternalScriptService { + + public ExternalAuthzDetailTypeService() { + super(CustomScriptType.AUTHZ_DETAIL); + } + + public Set getSupportedAuthzDetailsTypes() { + return customScriptConfigurationsNameMap.keySet(); + } + + public void externalValidateAuthzDetails(AuthzRequest authzRequest) { + ExecutionContext executionContext = ExecutionContext.of(authzRequest); + externalValidateAuthzDetails(executionContext); + } + + public void externalValidateAuthzDetails(ExecutionContext executionContext) { + final AuthzDetails authzDetails = executionContext.getAuthzDetails(); + for (AuthzDetail authzDetail : authzDetails.getDetails()) { + validateSingleAuthzDetail(executionContext, authzDetail); + } + } + + private void validateSingleAuthzDetail(ExecutionContext executionContext, AuthzDetail authzDetail) { + executionContext.setAuthzDetail(authzDetail); + + final String type = authzDetail.getType(); + final CustomScriptConfiguration script = getCustomScriptConfigurationByName(type); + if (script == null) { + log.error("Unable to find 'AuthzDetailType' custom script by name {}", type); + + throw executionContext.getAuthzRequest().getRedirectUriResponse().createWebException(AuthorizeErrorResponseType.ACCESS_DENIED, + "Unable to find 'AuthzDetailType' custom script by name " + type); + } + + externalValidateDetail(executionContext, script); + } + + public void externalValidateDetail(ExecutionContext executionContext, CustomScriptConfiguration script) { + log.trace("Executing python 'validateDetail' method, script name: {}, clientId: {}, authzDetail: {}", + script.getName(), executionContext.getClient().getClientId(), executionContext.getAuthzDetail()); + + executionContext.setScript(script); + + boolean result = false; + try { + AuthzDetailType authzDetailType = (AuthzDetailType) script.getExternalType(); + final ExternalScriptContext scriptContext = new ExternalScriptContext(executionContext); + result = authzDetailType.validateDetail(scriptContext); + + scriptContext.throwWebApplicationExceptionIfSet(); + } catch (WebApplicationException e) { + if (log.isTraceEnabled()) { + log.trace("WebApplicationException from script", e); + } + throw e; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + } + + log.trace("Finished 'validateDetail' method, script name: {}, clientId: {}, result: {}", script.getName(), executionContext.getClient().getClientId(), result); + + if (!result) { + throw executionContext.getAuthzRequest().getRedirectUriResponse().createWebException(AuthorizeErrorResponseType.ACCESS_DENIED, + "Access is denied by 'AuthzDetailType' custom script 'validateDetail' method."); + } + } + + public String externalGetUiRepresentation(ExecutionContext executionContext, AuthzDetail detail) { + executionContext.setAuthzDetail(detail); + + final String type = detail.getType(); + final CustomScriptConfiguration script = getCustomScriptConfigurationByName(type); + if (script == null) { + log.error("Unable to find 'AuthzDetailType' custom script by name {}", type); + + return detail.getJsonObject().toString(); + } + + return externalGetUiRepresentation(executionContext, script); + } + + public String externalGetUiRepresentation(ExecutionContext executionContext, CustomScriptConfiguration script) { + log.trace("Executing python 'getUiRepresentation' method, script name: {}, clientId: {}, authzDetail: {}", + script.getName(), executionContext.getAuthzRequest().getClientId(), executionContext.getAuthzDetail()); + + executionContext.setScript(script); + + String result = executionContext.getAuthzDetail().toString(); + try { + AuthzDetailType authzDetailType = (AuthzDetailType) script.getExternalType(); + final ExternalScriptContext scriptContext = new ExternalScriptContext(executionContext); + result = authzDetailType.getUiRepresentation(scriptContext); + + scriptContext.throwWebApplicationExceptionIfSet(); + } catch (WebApplicationException e) { + if (log.isTraceEnabled()) { + log.trace("WebApplicationException from script", e); + } + throw e; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + } + + log.trace("Finished 'getUiRepresentation' method, script name: {}, clientId: {}, result: {}", script.getName(), executionContext.getAuthzRequest().getClientId(), result); + + return result; + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java index db30f42535d..7f58f892e00 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java @@ -6,6 +6,7 @@ package io.jans.as.server.service.external.context; +import io.jans.as.model.authzdetails.AuthzDetail; import io.jans.as.model.util.Util; import io.jans.as.server.authorize.ws.rs.AuthzRequest; import io.jans.as.server.model.common.ExecutionContext; @@ -61,6 +62,10 @@ public AuthzRequest getAuthzRequest() { return executionContext != null ? executionContext.getAuthzRequest() : null; } + public AuthzDetail getAuthzDetail() { + return executionContext != null ? executionContext.getAuthzDetail() : null; + } + public PersistenceEntryManager getPersistenceEntryManager() { return persistenceEntryManager; } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/FapiOpenIdConfiguration.java b/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/FapiOpenIdConfiguration.java index e605b5f9cf1..9bf9f7617e0 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/FapiOpenIdConfiguration.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/FapiOpenIdConfiguration.java @@ -6,6 +6,7 @@ package io.jans.as.server.servlet; +import com.google.common.collect.Lists; import io.jans.as.common.model.registration.Client; import io.jans.as.common.service.AttributeService; import io.jans.as.model.common.GrantType; @@ -25,6 +26,7 @@ import io.jans.as.server.service.ClientService; import io.jans.as.server.service.ScopeService; import io.jans.as.server.service.external.ExternalAuthenticationService; +import io.jans.as.server.service.external.ExternalAuthzDetailTypeService; import io.jans.as.server.service.external.ExternalDynamicScopeService; import io.jans.as.server.service.token.TokenService; import io.jans.as.server.util.ServerUtil; @@ -79,6 +81,9 @@ public class FapiOpenIdConfiguration extends HttpServlet { @Inject private ExternalDynamicScopeService externalDynamicScopeService; + @Inject + private transient ExternalAuthzDetailTypeService externalAuthzDetailTypeService; + @Inject private CIBAConfigurationService cibaConfigurationService; @@ -245,6 +250,7 @@ protected void processRequest(HttpServletRequest servletRequest, HttpServletResp } jsonObj.put(ACR_VALUES_SUPPORTED, acrValuesSupported); jsonObj.put(AUTH_LEVEL_MAPPING, createAuthLevelMapping()); + Util.putArray(jsonObj, Lists.newArrayList(externalAuthzDetailTypeService.getSupportedAuthzDetailsTypes()), AUTHORIZATION_DETAILS_TYPES_SUPPORTED); JSONArray subjectTypesSupported = new JSONArray(); for (String subjectType : appConfiguration.getSubjectTypesSupported()) { diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/OpenIdConfiguration.java b/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/OpenIdConfiguration.java index 75819cb5484..5c7e1c768cc 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/OpenIdConfiguration.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/servlet/OpenIdConfiguration.java @@ -6,6 +6,7 @@ package io.jans.as.server.servlet; +import com.google.common.collect.Lists; import io.jans.as.common.service.AttributeService; import io.jans.as.model.common.*; import io.jans.as.model.configuration.AppConfiguration; @@ -17,6 +18,7 @@ import io.jans.as.server.service.LocalResponseCache; import io.jans.as.server.service.ScopeService; import io.jans.as.server.service.external.ExternalAuthenticationService; +import io.jans.as.server.service.external.ExternalAuthzDetailTypeService; import io.jans.as.server.service.external.ExternalDiscoveryService; import io.jans.as.server.service.external.ExternalDynamicScopeService; import io.jans.as.server.util.ServerUtil; @@ -71,6 +73,9 @@ public class OpenIdConfiguration extends HttpServlet { @Inject private transient ExternalDiscoveryService externalDiscoveryService; + @Inject + private transient ExternalAuthzDetailTypeService externalAuthzDetailTypeService; + @Inject private transient CIBAConfigurationService cibaConfigurationService; @@ -164,6 +169,7 @@ protected void processRequest(HttpServletRequest servletRequest, HttpServletResp jsonObj.put(AUTH_LEVEL_MAPPING, createAuthLevelMapping()); Util.putArray(jsonObj, getAcrValuesList(), ACR_VALUES_SUPPORTED); + Util.putArray(jsonObj, Lists.newArrayList(externalAuthzDetailTypeService.getSupportedAuthzDetailsTypes()), AUTHORIZATION_DETAILS_TYPES_SUPPORTED); Util.putArray(jsonObj, appConfiguration.getSubjectTypesSupported(), SUBJECT_TYPES_SUPPORTED); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenExchangeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenExchangeService.java index 9af2e141c28..2317df3f9b5 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenExchangeService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenExchangeService.java @@ -1,5 +1,6 @@ package io.jans.as.server.token.ws.rs; +import io.jans.as.model.authzdetails.AuthzDetails; import io.jans.as.common.model.registration.Client; import io.jans.as.common.model.session.SessionId; import io.jans.as.common.service.AttributeService; @@ -7,6 +8,7 @@ import io.jans.as.model.common.ScopeConstants; import io.jans.as.model.configuration.AppConfiguration; import io.jans.as.model.token.JsonWebResponse; +import io.jans.as.server.authorize.ws.rs.AuthzDetailsService; import io.jans.as.server.model.audit.OAuth2AuditLog; import io.jans.as.server.model.common.*; import io.jans.as.server.model.token.HandleTokenFactory; @@ -65,6 +67,9 @@ public class TokenExchangeService { @Inject private AttributeService attributeService; + @Inject + private AuthzDetailsService authzDetailsService; + public void rotateDeviceSecretOnRefreshToken(HttpServletRequest httpRequest, AuthorizationGrant refreshGrant, String scope) { if (StringUtils.isBlank(scope) || !scope.contains(ScopeConstants.DEVICE_SSO)) { log.debug("Skip rotate device secret on refresh token. No device_sso scope."); @@ -134,6 +139,7 @@ public JSONObject processTokenExchange(String scope, Function(Arrays.asList(scope.split(" "))) : new HashSet<>()); + final AuthzDetails authzDetails = authzDetailsService.validateAuthorizationDetails(authorizationDetails, executionContext); + executionContext.setAuthzDetails(authzDetails); + if (gt == GrantType.AUTHORIZATION_CODE) { return processAuthorizationCode(code, scope, codeVerifier, sessionIdObj, executionContext); } else if (gt == GrantType.REFRESH_TOKEN) { @@ -256,6 +265,7 @@ private Response processROPC(String username, String password, String scope, Gra RefreshToken reToken = tokenCreatorService.createRefreshToken(executionContext, scope); scope = resourceOwnerPasswordCredentialsGrant.checkScopesPolicy(scope); + AuthzDetails checkedAuthzDetails = authzDetailsService.checkAuthzDetailsAndSave(executionContext.getAuthzDetails(), resourceOwnerPasswordCredentialsGrant); AccessToken accessToken = resourceOwnerPasswordCredentialsGrant.createAccessToken(executionContext); // create token after scopes are checked @@ -282,13 +292,14 @@ private Response processROPC(String username, String password, String scope, Gra accessToken.getExpiresIn(), reToken, scope, - idToken)), executionContext.getAuditLog()); + idToken, checkedAuthzDetails)), executionContext.getAuditLog()); } private Response processClientGredentials(String scope, HttpServletRequest request, OAuth2AuditLog auditLog, Client client, Function idTokenPreProcessing, ExecutionContext executionContext) { ClientCredentialsGrant clientCredentialsGrant = authorizationGrantList.createClientCredentialsGrant(new User(), client); scope = clientCredentialsGrant.checkScopesPolicy(scope); + AuthzDetails checkedAuthzDetails = authzDetailsService.checkAuthzDetailsAndSave(executionContext.getAuthzDetails(), clientCredentialsGrant); executionContext.setGrant(clientCredentialsGrant); AccessToken accessToken = clientCredentialsGrant.createAccessToken(executionContext); // create token after scopes are checked @@ -315,7 +326,7 @@ private Response processClientGredentials(String scope, HttpServletRequest reque accessToken.getExpiresIn(), null, scope, - idToken)), auditLog); + idToken, checkedAuthzDetails)), auditLog); } private Response processRefreshTokenGrant(String scope, String refreshToken, Function idTokenPreProcessing, ExecutionContext executionContext) { @@ -345,6 +356,7 @@ private Response processRefreshTokenGrant(String scope, String refreshToken, Fun authorizationGrant.checkScopesPolicy(scope); scope = authorizationGrant.getScopesAsString(); + AuthzDetails checkedAuthzDetails = authzDetailsService.checkAuthzDetailsAndSave(executionContext.getAuthzDetails(), authorizationGrant); AccessToken accToken = authorizationGrant.createAccessToken(executionContext); // create token after scopes are checked @@ -378,7 +390,7 @@ private Response processRefreshTokenGrant(String scope, String refreshToken, Fun accToken.getExpiresIn(), reToken, scope, - idToken)), auditLog); + idToken, checkedAuthzDetails)), auditLog); } private Response processAuthorizationCode(String code, String scope, String codeVerifier, SessionId sessionIdObj, ExecutionContext executionContext) { @@ -400,6 +412,7 @@ private Response processAuthorizationCode(String code, String scope, String code RefreshToken reToken = tokenCreatorService.createRefreshToken(executionContext, scope); scope = authorizationCodeGrant.checkScopesPolicy(scope); + AuthzDetails checkedAuthzDetails = authzDetailsService.checkAuthzDetailsAndSave(executionContext.getAuthzDetails(), authorizationCodeGrant); AccessToken accToken = authorizationCodeGrant.createAccessToken(executionContext); // create token after scopes are checked final String deviceSecret = tokenExchangeService.createNewDeviceSecret(authorizationCodeGrant.getSessionDn(), client, authorizationCodeGrant.getScopesAsString()); @@ -434,7 +447,7 @@ private Response processAuthorizationCode(String code, String scope, String code JSONObject jsonObj = new JSONObject(); try { - fillJsonObject(jsonObj, accToken, accToken.getTokenType(), accToken.getExpiresIn(), reToken, scope, idToken); + fillJsonObject(jsonObj, accToken, accToken.getTokenType(), accToken.getExpiresIn(), reToken, scope, idToken, checkedAuthzDetails); if (StringUtils.isNotBlank(deviceSecret)) { jsonObj.put("device_token", deviceSecret); } @@ -514,6 +527,7 @@ private Response processDeviceCodeGrantType(ExecutionContext executionContext, f null, null, accessToken, refToken, null, executionContext); deviceCodeGrant.checkScopesPolicy(scope); + AuthzDetails checkedAuthzDetails = authzDetailsService.checkAuthzDetailsAndSave(executionContext.getAuthzDetails(), deviceCodeGrant); log.info("Device authorization in token endpoint processed and return to the client, device_code: {}", deviceCodeGrant.getDeviceCode()); @@ -522,7 +536,7 @@ private Response processDeviceCodeGrantType(ExecutionContext executionContext, f grantService.removeByCode(deviceCodeGrant.getDeviceCode()); return Response.ok().entity(getJSonResponse(accessToken, accessToken.getTokenType(), - accessToken.getExpiresIn(), refToken, scope, idToken)).build(); + accessToken.getExpiresIn(), refToken, scope, idToken, checkedAuthzDetails)).build(); } else { final DeviceAuthorizationCacheControl cacheData = deviceAuthorizationService.getDeviceAuthzByDeviceCode(deviceCode); log.trace("DeviceAuthorizationCacheControl data : '{}'", cacheData); @@ -575,10 +589,10 @@ private ResponseBuilder error(int status, TokenErrorResponseType type, String re */ public String getJSonResponse(AccessToken accessToken, TokenType tokenType, Integer expiresIn, RefreshToken refreshToken, String scope, - IdToken idToken) { + IdToken idToken, AuthzDetails checkedAuthzDetails) { JSONObject jsonObj = new JSONObject(); try { - fillJsonObject(jsonObj, accessToken, tokenType, expiresIn, refreshToken, scope, idToken); + fillJsonObject(jsonObj, accessToken, tokenType, expiresIn, refreshToken, scope, idToken, checkedAuthzDetails); } catch (JSONException e) { log.error(e.getMessage(), e); } @@ -588,7 +602,7 @@ public String getJSonResponse(AccessToken accessToken, TokenType tokenType, public static void fillJsonObject(JSONObject jsonObj, AccessToken accessToken, TokenType tokenType, Integer expiresIn, RefreshToken refreshToken, String scope, - IdToken idToken) { + IdToken idToken, AuthzDetails checkedAuthzDetails) { jsonObj.put("access_token", accessToken.getCode()); // Required jsonObj.put("token_type", tokenType.toString()); // Required if (expiresIn != null) { // Optional @@ -603,6 +617,9 @@ public static void fillJsonObject(JSONObject jsonObj, AccessToken accessToken, T if (idToken != null) { jsonObj.put("id_token", idToken.getCode()); } + if (checkedAuthzDetails != null && checkedAuthzDetails.getDetails() != null && !checkedAuthzDetails.getDetails().isEmpty()) { + jsonObj.put("authorization_details", checkedAuthzDetails.asJsonArray()); + } } private Response processCIBA(String scope, String authReqId, Function idTokenPreProcessing, ExecutionContext executionContext) { @@ -644,6 +661,7 @@ private Response processCIBA(String scope, String authReqId, Function + +
  • + + + + + +
    +
  • +
  • diff --git a/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-template.xhtml b/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-template.xhtml index ba5b6ea3300..b1bf65021d9 100644 --- a/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-template.xhtml +++ b/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-template.xhtml @@ -7,7 +7,7 @@ - oxAuth + Jans diff --git a/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/ciba-authorize-template.xhtml b/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/ciba-authorize-template.xhtml index b24ad8b12ac..74b55f3ddc2 100644 --- a/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/ciba-authorize-template.xhtml +++ b/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/ciba-authorize-template.xhtml @@ -7,7 +7,7 @@ - oxAuth + Jans diff --git a/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/template.xhtml b/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/template.xhtml index 6767e978096..3fc7a95de62 100644 --- a/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/template.xhtml +++ b/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/template.xhtml @@ -7,7 +7,7 @@ - oxAuth + Jans diff --git a/jans-auth-server/server/src/main/webapp/authorize.xhtml b/jans-auth-server/server/src/main/webapp/authorize.xhtml index 95489f040d9..ac8647d8942 100644 --- a/jans-auth-server/server/src/main/webapp/authorize.xhtml +++ b/jans-auth-server/server/src/main/webapp/authorize.xhtml @@ -34,6 +34,7 @@ + @@ -46,6 +47,7 @@ + authorizeRestWebServiceValidator.validateNotWebView(httpServletRequest)); verify(log).error(anyString(), eq(testPackage)); } + + @Test + public void validateAuthorizationDetails_withoutAuthzDetails_shouldPassSuccessfully() { + AuthzRequest authzRequest = new AuthzRequest(); + Client client = new Client(); + + authorizeRestWebServiceValidator.validateAuthorizationDetails(authzRequest, client); + } + + @Test + public void validateAuthorizationDetails_withInvalidAuthzDetails_throwException() { + final RedirectUri redirectUri = mock(RedirectUri.class); + when(redirectUri.toString()).thenReturn("http://rp.com"); + + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setAuthzDetailsString("not_valid_json"); + authzRequest.setRedirectUriResponse(new RedirectUriResponse(redirectUri, "", mock(HttpServletRequest.class), mock(ErrorResponseFactory.class))); + Client client = new Client(); + + assertThrows(WebApplicationException.class, () -> authorizeRestWebServiceValidator.validateAuthorizationDetails(authzRequest, client)); + } + + @Test + public void validateAuthorizationDetails_withNotSupportedScriptType_throwException() { + final RedirectUri redirectUri = mock(RedirectUri.class); + when(redirectUri.toString()).thenReturn("http://rp.com"); + + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setAuthzDetailsString("[{\"type\":\"internal_type\"}]"); + authzRequest.setRedirectUriResponse(new RedirectUriResponse(redirectUri, "", mock(HttpServletRequest.class), mock(ErrorResponseFactory.class))); + Client client = new Client(); + + assertThrows(WebApplicationException.class, () -> authorizeRestWebServiceValidator.validateAuthorizationDetails(authzRequest, client)); + } + + @Test + public void validateAuthorizationDetails_withNotSupportedClientType_throwException() { + final RedirectUri redirectUri = mock(RedirectUri.class); + when(redirectUri.toString()).thenReturn("http://rp.com"); + when(externalAuthzDetailTypeService.getSupportedAuthzDetailsTypes()).thenReturn(new HashSet<>(Collections.singletonList("internal_type"))); + + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setAuthzDetailsString("[{\"type\":\"internal_type\"}]"); + authzRequest.setRedirectUriResponse(new RedirectUriResponse(redirectUri, "", mock(HttpServletRequest.class), mock(ErrorResponseFactory.class))); + Client client = new Client(); + + assertThrows(WebApplicationException.class, () -> authorizeRestWebServiceValidator.validateAuthorizationDetails(authzRequest, client)); + } + + @Test + public void validateAuthorizationDetails_withSupportedClientAndScriptType_shouldPassSuccessfully() { + final RedirectUri redirectUri = mock(RedirectUri.class); + when(externalAuthzDetailTypeService.getSupportedAuthzDetailsTypes()).thenReturn(new HashSet<>(Collections.singletonList("internal_type"))); + + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setAuthzDetailsString("[{\"type\":\"internal_type\"}]"); + authzRequest.setRedirectUriResponse(new RedirectUriResponse(redirectUri, "", mock(HttpServletRequest.class), mock(ErrorResponseFactory.class))); + Client client = new Client(); + client.getAttributes().setAuthorizationDetailsTypes(Collections.singletonList("internal_type")); + + authorizeRestWebServiceValidator.validateAuthorizationDetails(authzRequest, client); + } } diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthzDetailsServiceTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthzDetailsServiceTest.java new file mode 100644 index 00000000000..ef21f8d7a9f --- /dev/null +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthzDetailsServiceTest.java @@ -0,0 +1,131 @@ +package io.jans.as.server.authorize.ws.rs; + +import io.jans.as.model.authzdetails.AuthzDetails; +import io.jans.as.common.model.registration.Client; +import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.server.model.common.ExecutionContext; +import io.jans.as.server.service.external.ExternalAuthzDetailTypeService; +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; + +import java.util.Collections; +import java.util.HashSet; + +import static org.mockito.Mockito.when; +import static org.testng.Assert.*; + +/** + * @author Yuriy Z + */ +@Listeners(MockitoTestNGListener.class) +public class AuthzDetailsServiceTest { + + @InjectMocks + private AuthzDetailsService authzDetailsService; + + @Mock + private Logger log; + + @Mock + private ExternalAuthzDetailTypeService externalAuthzDetailTypeService; + + @Mock + private ErrorResponseFactory errorResponseFactory; + + @Test + public void checkAuthzDetails_whenRequestedAuthzDetailsIsWiderThenAuthorizedDetails_shouldNarrowGrantedDetails() { + String requested = "[\n" + + " {\n" + + " \"type\": \"internal_a1\"\n" + + " },\n" + + " {\n" + + " \"type\": \"internal_a2\"\n" + + " }\n" + + "]"; + + String authorized = "[\n" + + " {\n" + + " \"type\": \"internal_a1\"\n" + + " },\n" + + " {\n" + + " \"type\": \"internal_a3\"\n" + + " }\n" + + "]"; + + String granted = "[\n" + + " {\n" + + " \"type\": \"internal_a1\"\n" + + " }\n" + + "]"; + + final AuthzDetails grantedDetails = authzDetailsService.checkAuthzDetails(AuthzDetails.of(requested), AuthzDetails.of(authorized)); + assertNotNull(grantedDetails); + assertTrue(grantedDetails.similar(granted)); // it must reflect authorized + assertFalse(grantedDetails.similar(requested)); // it must not be similar to requested + } + + @Test + public void checkAuthzDetails_whenAuthzDetailsIsEmpty_shouldReturnNull() { + assertNull(authzDetailsService.checkAuthzDetails(null, null)); + } + + @Test + public void validateAuthorizationDetails_withoutAuthzDetails_shouldPassSuccessfully() { + Client client = new Client(); + + ExecutionContext executionContext = new ExecutionContext(); + executionContext.setClient(client); + + authzDetailsService.validateAuthorizationDetails("", executionContext); + } + + @Test + public void validateAuthorizationDetails_withInvalidAuthzDetails_throwException() { + Client client = new Client(); + + ExecutionContext executionContext = new ExecutionContext(); + executionContext.setClient(client); + + assertThrows(WebApplicationException.class, () -> authzDetailsService.validateAuthorizationDetails("not_valid_json", executionContext)); + } + + @Test + public void validateAuthorizationDetails_withNotSupportedScriptType_throwException() { + Client client = new Client(); + + ExecutionContext executionContext = new ExecutionContext(); + executionContext.setClient(client); + + assertThrows(WebApplicationException.class, () -> authzDetailsService.validateAuthorizationDetails("[{\"type\":\"internal_type\"}]", executionContext)); + } + + @Test + public void validateAuthorizationDetails_withNotSupportedClientType_throwException() { + Client client = new Client(); + + ExecutionContext executionContext = new ExecutionContext(); + executionContext.setClient(client); + + when(externalAuthzDetailTypeService.getSupportedAuthzDetailsTypes()).thenReturn(new HashSet<>(Collections.singletonList("internal_type"))); + + assertThrows(WebApplicationException.class, () -> authzDetailsService.validateAuthorizationDetails("[{\"type\":\"internal_type\"}]", executionContext)); + } + + @Test + public void validateAuthorizationDetails_withSupportedClientAndScriptType_shouldPassSuccessfully() { + Client client = new Client(); + client.getAttributes().setAuthorizationDetailsTypes(Collections.singletonList("internal_type")); + + ExecutionContext executionContext = new ExecutionContext(); + executionContext.setClient(client); + + when(externalAuthzDetailTypeService.getSupportedAuthzDetailsTypes()).thenReturn(new HashSet<>(Collections.singletonList("internal_type"))); + + authzDetailsService.validateAuthorizationDetails("[{\"type\":\"internal_type\"}]", executionContext); + } +} diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/token/ws/rs/TokenExchangeServiceTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/token/ws/rs/TokenExchangeServiceTest.java index 5abd94ec9b9..344c5c033a7 100644 --- a/jans-auth-server/server/src/test/java/io/jans/as/server/token/ws/rs/TokenExchangeServiceTest.java +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/token/ws/rs/TokenExchangeServiceTest.java @@ -7,6 +7,7 @@ import io.jans.as.model.crypto.AbstractCryptoProvider; import io.jans.as.model.error.ErrorResponseFactory; import io.jans.as.server.audit.ApplicationAuditLogger; +import io.jans.as.server.authorize.ws.rs.AuthzDetailsService; import io.jans.as.server.service.SessionIdService; import org.apache.commons.lang.StringUtils; import org.mockito.InjectMocks; @@ -45,6 +46,9 @@ public class TokenExchangeServiceTest { @Mock private SessionIdService sessionIdService; + @Mock + private AuthzDetailsService authzDetailsService; + @InjectMocks private TokenExchangeService tokenExchangeService; diff --git a/jans-auth-server/server/src/test/resources/testng.xml b/jans-auth-server/server/src/test/resources/testng.xml index 6fae5afad96..7ef0cd7b603 100644 --- a/jans-auth-server/server/src/test/resources/testng.xml +++ b/jans-auth-server/server/src/test/resources/testng.xml @@ -35,6 +35,7 @@ + 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 33d55c3be75..7f4ffb8037c 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 @@ -15,6 +15,8 @@ import io.jans.model.custom.script.type.authz.DummyConsentGatheringType; import io.jans.model.custom.script.type.authzchallenge.AuthorizationChallengeType; import io.jans.model.custom.script.type.authzchallenge.DummyAuthorizationChallengeType; +import io.jans.model.custom.script.type.authzdetails.AuthzDetailType; +import io.jans.model.custom.script.type.authzdetails.DummyAuthzDetail; import io.jans.model.custom.script.type.ciba.DummyEndUserNotificationType; import io.jans.model.custom.script.type.ciba.EndUserNotificationType; import io.jans.model.custom.script.type.client.ClientRegistrationType; @@ -105,6 +107,7 @@ public enum CustomScriptType implements AttributeEnum { PERSISTENCE_EXTENSION("persistence_extension", "Persistence Extension", PersistenceType.class, CustomScript.class, "PersistenceExtension", new DummyPeristenceType()), IDP("idp", "Idp Extension", IdpType.class, CustomScript.class, "IdpExtension", new DummyIdpType()), DISCOVERY("discovery", "Discovery", DiscoveryType.class, CustomScript.class, "Discovery", new DummyDiscoveryType()), + AUTHZ_DETAIL("authz_detail", "Authorization Detail", AuthzDetailType.class, CustomScript.class, "AuthzDetail", new DummyAuthzDetail()), UPDATE_TOKEN("update_token", "Update Token", UpdateTokenType.class, CustomScript.class, "UpdateToken", new DummyUpdateTokenType()), CONFIG_API("config_api_auth", "Config Api Auth", ConfigApiType.class, CustomScript.class,"ConfigApiAuthorization", new DummyConfigApiType()), MODIFY_SSA_RESPONSE("modify_ssa_response", "Modify SSA Response", ModifySsaResponseType.class, CustomScript.class, "ModifySsaResponse", new DummyModifySsaResponseType()), diff --git a/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzdetails/AuthzDetailType.java b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzdetails/AuthzDetailType.java new file mode 100644 index 00000000000..0b453af9b57 --- /dev/null +++ b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzdetails/AuthzDetailType.java @@ -0,0 +1,16 @@ +package io.jans.model.custom.script.type.authzdetails; + +import io.jans.model.custom.script.type.BaseExternalType; + +/** + * Authorization Details Script + * + * @author Yuriy Z + */ +public interface AuthzDetailType extends BaseExternalType { + + boolean validateDetail(Object context); + + String getUiRepresentation(Object context); +} + diff --git a/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzdetails/DummyAuthzDetail.java b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzdetails/DummyAuthzDetail.java new file mode 100644 index 00000000000..ce6e4744aa6 --- /dev/null +++ b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzdetails/DummyAuthzDetail.java @@ -0,0 +1,41 @@ +package io.jans.model.custom.script.type.authzdetails; + +import io.jans.model.SimpleCustomProperty; +import io.jans.model.custom.script.model.CustomScript; + +import java.util.Map; + +/** + * @author Yuriy Z + */ +public class DummyAuthzDetail implements AuthzDetailType { + @Override + public boolean init(Map configurationAttributes) { + return false; + } + + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + return false; + } + + @Override + public boolean destroy(Map configurationAttributes) { + return false; + } + + @Override + public int getApiVersion() { + return 0; + } + + @Override + public boolean validateDetail(Object context) { + return true; + } + + @Override + public String getUiRepresentation(Object context) { + return ""; + } +} diff --git a/jans-linux-setup/jans_setup/schema/jans_schema.json b/jans-linux-setup/jans_setup/schema/jans_schema.json index 3611734ec17..5b6e3303220 100644 --- a/jans-linux-setup/jans_setup/schema/jans_schema.json +++ b/jans-linux-setup/jans_setup/schema/jans_schema.json @@ -4593,7 +4593,8 @@ "jansUsrId", "exp", "del", - "jansScope" + "jansScope", + "jansAttrs" ], "must": [ "objectclass" diff --git a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-errors.json b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-errors.json index ca695b2861e..f698a00f2f3 100644 --- a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-errors.json +++ b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-errors.json @@ -50,6 +50,11 @@ "description":"The redirect_uri in the Authorization Request does not match any of the Client's pre-registered redirect_uris.", "uri":null }, + { + "id":"invalid_authorization_details", + "description":"The authorization_details in the Authorization Request does not pass AS validation.", + "uri":null + }, { "id":"login_required", "description":"The Authorization Server requires End-User authentication. This error MAY be returned when the prompt parameter in the Authorization Request is set to none to request that the Authorization Server should not display any user interfaces to the End-User, but the Authorization Request cannot be completed without displaying a user interface for user authentication.", @@ -193,6 +198,11 @@ "description":"Authorization server requires nonce in DPoP proof.", "uri":null }, + { + "id":"invalid_authorization_details", + "description":"The authorization_details in the request does not pass AS validation.", + "uri":null + }, { "id":"use_new_dpop_nonce", "description":"Authorization server requires new nonce in DPoP proof.", diff --git a/jans-linux-setup/jans_setup/templates/scripts.ldif b/jans-linux-setup/jans_setup/templates/scripts.ldif index 427afaa682d..8cf3add8f6b 100644 --- a/jans-linux-setup/jans_setup/templates/scripts.ldif +++ b/jans-linux-setup/jans_setup/templates/scripts.ldif @@ -536,6 +536,20 @@ jansRevision: 11 jansScr::%(discovery_discovery)s jansScrTyp: discovery +dn: inum=0300-BA98,ou=scripts,o=jans +objectClass: jansCustomScr +objectClass: top +description: Demo Authz Detail Java Script +displayName: demo_authz_detail +inum: 0300-BA98 +jansEnabled: true +jansLevel: 1 +jansModuleProperty: {"value1":"location_type","value2":"db","description":""} +jansProgLng: java +jansRevision: 11 +jansScr::%(authz_detail_authzdetail)s +jansScrTyp: authz_detail + dn: inum=0300-BA99,ou=scripts,o=jans objectClass: jansCustomScr objectClass: top diff --git a/mkdocs.yml b/mkdocs.yml index b055187e038..77cc02cb6c6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -186,6 +186,7 @@ nav: - OpenID Userinfo Token: admin/auth-server/tokens/openid-userinfo-token.md - UMA RPT Token: admin/auth-server/tokens/uma-rpt-token.md - Scopes: admin/auth-server/scopes/README.md + - Rich Authorization Requests : admin/auth-server/authz-details/README.md - Endpoints: - admin/auth-server/endpoints/README.md - OpenID Configuration: admin/auth-server/endpoints/configuration.md