From 0a46c8687f619827f2d9775cd20572818d100c8c Mon Sep 17 00:00:00 2001 From: Markus Strehle <11627201+strehle@users.noreply.github.com> Date: Tue, 17 Dec 2024 09:52:35 +0100 Subject: [PATCH] Fix workload identity (#3197) * Fix workload identity https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation * Add documentation * Enhance uaa client authentication document --- docs/UAA-Client-Authentication.md | 46 +++++++++++++++---- .../oauth/jwt/JwtClientAuthentication.java | 3 +- .../IdentityProviderEndpointDocs.java | 2 + 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/docs/UAA-Client-Authentication.md b/docs/UAA-Client-Authentication.md index ec5661c8313..c5989a2352d 100644 --- a/docs/UAA-Client-Authentication.md +++ b/docs/UAA-Client-Authentication.md @@ -1,25 +1,25 @@ # UAA Client Authentication -UAA acts as OAuth2 / OIDC server and this requires the separation of users and clients. This document focuses on -the clients and in detail on the client authentication, because this has special behaviors. -In [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749#section-2.3.1) the password of a client is specified as so called client_secret. Its possession +UAA acts as OAuth2 / OIDC server and this requires the separate authentication of users and clients. This document focuses on +the clients and in detail on the key based client authentication, because this has special behaviors. +In [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749#section-2.3.1) the password of a client is specified as so-called secret (parameter client_secret). Its possession or better the process of checking its possession means the authentication process. The secrets can be passed to a server in different ways. It can happen through HTTP header and/or the body. In case of header the Authorization header is used, but the encoding of the secret needs to be done according to the RFC 6749. UAA fixed this behavior with https://github.com/cloudfoundry/uaa/issues/778. The OIDC standard defines more authentication mechanism, see [section 9](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication). -The usage of secrets via client_secret_basic and client_secret_post is easy to setup and easy to use, however if system to system communication is +The usage of secrets via client_secret_basic and client_secret_post is easy to set up and easy to use, however if system to system communication is in use, this can get a security problem, because it will be hard to change secrets in running systems. The use of many secrets is not supported, also because the check can only be done sequentially. The exchange of a secret is a security problem in it self. Therefore the newer standards define further token based authentication mechanism for OAuth2 clients. They are: -* private_key_jwt [OIDC core standard](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication) +* private_key_jwt [OIDC core standard](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication) and [RFC 7523 from OAuth2 standard](https://www.rfc-editor.org/info/rfc7523) * tls_client_auth [RFC 8705](https://www.rfc-editor.org/rfc/rfc8705) ## New methods The new methods are based on asymmetric trust relation, so that the keys are divided into a private and a public one. The private key should never leave the original system, but only the public key should be exchanged. -### private_key_jwt (Experimental Feature) +### private_key_jwt (Partly finished) The standard private_key_jwt is similar to the existing JWT bearer flow, but JWT bearer is for user principle propagation, whereas private_key_jwt is used for client authentication only. The used technics are similar and therefore the trust model is similar. Both usages are specified in the same [RFC 7523](https://www.rfc-editor.org/rfc/rfc7523.txt). The JWT bearer trust is based on parameters tokenKey and/or tokenKeyUrl parameter, part of the @@ -28,13 +28,22 @@ of public keys and this set can contain many keys because each key has its own k a dynamic token key URI. OIDC has defined the parameter jwks_uri for this already. The structure of the keys is defined with [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517). UAA provides its own jwks_uri with endpoint /token_keys. The content of this endpoint is [JWKS](https://datatracker.ietf.org/doc/html/rfc7517#section-5). -The new parameters for UAA clients are: +The content of the JWT (parameter client_assertion) can be different. The difference is defined by the standards. The [OIDC core standard](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication) +simplify the structure, so that sub, iss content are the client_id of the authenticated OAuth2 client. The key rotation is supported with +jwks_uri, which retrieves the JWK. You can only have one JWKS_URI by the client. For the [RFC 7523 from OAuth2 standard](https://www.rfc-editor.org/info/rfc7523) the +structure is more complex but with seperated iss and sub there can be more than one entry of federated credential. + +The new parameters for JWKS Trust in UAA clients are: * jwks_uri * jwks This should allow a continuous trust between a UAA to UAA communication, e.g. using own UAA instances or within a UAA using different zones. +The new parameter for federated Credentials in UAA clients is (Work in progress parameter): + +* fed_creds + ### tls_client_auth (Planned Feature) Not yet defined a release date. @@ -70,8 +79,26 @@ oauth: ``` The example config above with jwks_uri enables continuous trust to a running uaa. +Here is a brief example of the oauth providers section, where UAA is acting as client. +```yaml +login: + oauth: + providers: + uaa.proxy: + type: oidc1.0 + passwordGrantEnabled: true + discoveryUrl: http://localhost:8080/uaa/oauth/token/.well-known/openid-configuration + issuer: http://localhost:8080/uaa/oauth/token + linkText: Login with another UAA + relyingPartyId: uaa-trust-uri + jwtClientAuthentication: true + showLinkText: true +``` + +The option jwtClientAuthentication creates during the proxy flow a client assertion which is based on OIDC private_key_jwt. + ### Developer implementation -As deverloper you should use the [UAA documention](https://docs.cloudfoundry.org/api/uaa/version/76.23.0/index.html#token). There is a description +As developer, you should use the [UAA documentation](https://docs.cloudfoundry.org/api/uaa/version/77.18.0/index.html#token). There is a description about the new parameters client_assertion and client_assertion_type. In addition, you can check in the retrieved access_token tokens for the existence of claim client_auth_method with value private_key_jwt, (client_auth_method=private_key). This claim should guarantee the used method of client authentication. Tokens without this claim are authenticated with secrets. There might be use-cases where a stronger authentication mechanism is @@ -79,5 +106,4 @@ required. ### Production use -The support of private_key_jwt for a production is planned with end of Q4/2023. The use for other purposes is given with a release of UAA starting -from version 76.23.0 or higher. +The support of private_key_jwt (according to OIDC) for a production is given with end of Q4/2024. diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtClientAuthentication.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtClientAuthentication.java index dc9f31f7055..444952daf74 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtClientAuthentication.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtClientAuthentication.java @@ -83,12 +83,13 @@ public String getClientAssertion( final boolean allowDynamicValueLookupInCustomZone ) { HashMap jwtClientConfiguration = Optional.ofNullable(getJwtClientConfigurationElements(config.getJwtClientAuthentication())).orElse(new HashMap<>()); + String subject = readJwtClientOption(jwtClientConfiguration.get("sub"), config.getRelyingPartyId(), allowDynamicValueLookupInCustomZone); String issuer = readJwtClientOption(jwtClientConfiguration.get("iss"), config.getRelyingPartyId(), allowDynamicValueLookupInCustomZone); String audience = readJwtClientOption(jwtClientConfiguration.get("aud"), config.getTokenUrl().toString(), allowDynamicValueLookupInCustomZone); String kid = readJwtClientOption(jwtClientConfiguration.get("kid"), keyInfoService.getActiveKey().keyId(), allowDynamicValueLookupInCustomZone); Claims claims = new Claims(); claims.setAud(Arrays.asList(audience)); - claims.setSub(config.getRelyingPartyId()); + claims.setSub(subject); claims.setIss(issuer); claims.setJti(UUID.randomUUID().toString().replace("-", "")); claims.setIat((int) Instant.now().minusSeconds(120).getEpochSecond()); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index 59eb027a818..6598351cec5 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -611,6 +611,7 @@ void createOAuthIdentityProvider() throws Exception { "

" + "The values in the list can be a reference to another section in uaa yaml, e.g. define for key a reference like ${\"jwt.client.key\"}. This will load the private key from yaml context jwt.client.key. The advantage is, that you can use a single key for many IdP configurations and the key itself is not persistent in the UAA DB.

"), @@ -722,6 +723,7 @@ void createOidcIdentityProvider() throws Exception { "

" + "The values in the list can be a reference to another section in uaa yaml, e.g. define for key a reference like ${\"jwt.client.key\"}. This will load the private key from yaml context jwt.client.key. The advantage is, that you can use a single key for many IdP configurations and the key itself is not persistent in the UAA DB.

"),