[ https://issues.apache.org/jira/browse/NIFI-8406?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel ]
Vijay Jammi updated NIFI-8406: ------------------------------ Description: The current oidc client authentication methods (client_secret_post, client_secret_basic) require client credentials (client_secret) to be stored as plain text on the client's filesystem, which could also be inadvertently checked into source control system. Due to these and other security considerations, we should be able to use assertions as client credentials for authenticating against the token endpoint. While using assertions an oidc client will include client_assertion and client_assertion_type parameters instead of passing the client_secret for authentication. Details on the OAuth 2.0 specifications for client authentication using jwt and assertions can be found under # [RFC 7523, Section 2.2 (Using JWTs for Client Authentication)|https://tools.ietf.org/html/rfc7523#section-2.2] # [RFC 7521 (Using Assertions for Client Authentication)|https://tools.ietf.org/html/rfc7521#section-4.2]. Summary of the changes needed for assertion based client authentication. * Generate a pair of private and public key which can be made available to Nifi via a keystore on the filesystem. * Make the public key or cert available to the Authorization Server. Alternatively to allow key rotations, we could configure a JWKS URL within Nifi to allow the authorization server download the public key using the keyId (kid). * While building the token request against the token endpoint, need StandardOidcIdentityProvider to provide a way to build a private_key_jwt based client authentication in addition to the existing client_secret_basic, client_secret_post. {code:java} if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(clientAuthenticationMethod)){ clientAuthentication = new PrivateKeyJWT(clientId, tokenEndpoint, jwsAlgorithm, privateKey, keyId, null); }else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthenticationMethod)) { clientAuthentication = new ClientSecretPost(clientId, clientSecret); } else { clientAuthentication = new ClientSecretBasic(clientId, clientSecret); }{code} * Encode and sign the JWT token with the private key, conforming to RFC 7523, section 2.2 and pass the signed token within the client_assertion request parameter along with the client_assertion_type. {code:java} POST https://<auth-host>:<port>/.../token HTTP/1.1 Content-Type: application/x-www-form-urlencoded client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer& client_assertion=eyJraWQiOiJkMlY2LS1OUG....hTMfA& grant_type=authorization_code& code=9ba8d85b-...-c7a6fb6dae55 {code} * The Authorization Server will use the client's public key or cert to verify the signature of the JWT and issues the token Nifi Token Exchange Process When authentication within Nifi is enabled via OpenId Connect, a user is re-directed to the configured authorization endpoint (authorization server) via Nifi's oidc/request endpoint. The user upon successfully authenticating with the authorization server is directed back to Nifi's (oidc/callback endpoint) with a temporary authorization code. This authorization code is then exchanged for an ID Token by the OidcService via the Oidc Identity Provider (StandardOidcIdentityProvider - exchangeAuthorizationCode(authorizationGrant)). The identity provider requests this exchange by authenticating itself against the token endpoint of the authorization server and presenting the authorization grant. The authorization server authenticates the client, validates the authorization grant and upon successful validation it issues an access token to Nifi. Currently, the exchangeAuthorizationCode(...) on StandardOidcIdentityProvider provides two methods of Client Authentication (client_secret_basic, client_secret_post) which are both based on the Authorization Server issuing a set of client credentials (client_secret) which are presented to the authorization server as client credentials during token exchange. {code:java} public String exchangeAuthorizationCode(final AuthorizationGrant authorizationGrant) throws IOException {public String exchangeAuthorizationCode(final AuthorizationGrant authorizationGrant) throws IOException { ... // 1 - Build the client authentication using the clientId and clientSecret obtained during the registration of the client with the authorization server if (oidcProviderMetadata.getTokenEndpointAuthMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_POST)) { clientAuthentication = new ClientSecretPost(clientId, clientSecret); } else { clientAuthentication = new ClientSecretBasic(clientId, clientSecret); } // 2 - Build the token endpoint request with the above client authentication TokenRequest request = new TokenRequest(tokenEndpoint, clientAuthentication, authorizationGrant); HTTPRequest tokenHttpRequest = request.toHTTPRequest(); ... // 3 - Submit the token request and Get the token response TokenResponse response = OIDCTokenResponseParser.parse(tokenHttpRequest.send()); // 4 - Upon Successful Response get the ID Token and the User Identity if (response.indicatesSuccess()) { final OIDCTokenResponse oidcTokenResponse = (OIDCTokenResponse) response; final OIDCTokens oidcTokens = oidcTokenResponse.getOIDCTokens(); // validate the access token and parse the claims final IDTokenClaimsSet claimsSet = tokenValidator.validate(oidcJwt, null); // attempt to extract the configured claim to access the user's identity; default is 'email' String identity = claimsSet.getStringClaim(properties.getOidcClaimIdentifyingUser()); ... } // 5 - If User Identity could not extracted out of the IDToken, lookup the identity against the UserInfo endpoint if (StringUtils.isBlank(identity)) { ... final BearerAccessToken bearerAccessToken = oidcTokens.getBearerAccessToken(); ... // 7 - Invoke the UserInfo endpoint to lookup user identity against the UserInfo endpoint identity = lookupIdentityInUserInfoModified(bearerAccessToken); ... } ... // 8 - Convert into a Nifi jwt for retrieval later final LoginAuthenticationToken loginToken = new LoginAuthenticationToken(identity, identity, expiresIn, claimsSet.getIssuer().getValue()); return jwtService.generateSignedToken(loginToken);} {code} was: The current oidc client authentication methods (client_secret_post, client_secret_basic) require client credentials (client_secret) to be stored as plain text on the client's filesystem, which could also be inadvertently checked into source control system. Due to these and other security considerations, we should be able to use assertions as client credentials for authenticating against the token endpoint. While using assertions an oidc client will include client_assertion and client_assertion_type parameters instead of passing the client_secret for authentication. Details on the OAuth 2.0 specifications for client authentication using jwt and assertions can be found under # [RFC 7523, Section 2.2 (Using JWTs for Client Authentication)|https://tools.ietf.org/html/rfc7523#section-2.2] # [RFC 7521 (Using Assertions for Client Authentication)|https://tools.ietf.org/html/rfc7521#section-4.2]. Summary of the changes needed for assertion based client authentication. * Generate a pair of private and public key which can be made available to Nifi via a keystore on the filesystem. * Make the public key or cert available to the Authorization Server. Alternatively to allow key rotations, we could configure a JWKS URL within Nifi to allow the authorization server download the public key using the keyId (kid). * While building the token request against the token endpoint, need StandardOidcIdentityProvider to provide a way to build a private_key_jwt based client authentication in addition to the existing client_secret_basic, client_secret_post. {code:java} if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(clientAuthenticationMethod)){ clientAuthentication = new PrivateKeyJWT(clientId, tokenEndpoint, jwsAlgorithm, privateKey, keyId, null); }else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthenticationMethod)) { clientAuthentication = new ClientSecretPost(clientId, clientSecret); } else { clientAuthentication = new ClientSecretBasic(clientId, clientSecret); }{code} * Encode and sign the JWT token with the private key, conforming to RFC 7523, section 2.2 and pass the signed token within the client_assertion request parameter along with the client_assertion_type. {code:java} POST https://<auth-host>:<port>/.../token HTTP/1.1 Content-Type: application/x-www-form-urlencoded client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer& client_assertion=eyJraWQiOiJkMlY2LS1OUG....hTMfA& grant_type=authorization_code& code=9ba8d85b-...-c7a6fb6dae55 {code} * The Authorization Server will use the client's public key or cert to verify the signature of the JWT and issues the token > Oidc Identity Provider should support assertions as client credentials for > authenticating against the token endpoint > -------------------------------------------------------------------------------------------------------------------- > > Key: NIFI-8406 > URL: https://issues.apache.org/jira/browse/NIFI-8406 > Project: Apache NiFi > Issue Type: Improvement > Components: Core Framework, Security > Affects Versions: 1.11.4 > Reporter: Vijay Jammi > Priority: Major > Labels: OIDC, Security, assertion > > The current oidc client authentication methods (client_secret_post, > client_secret_basic) require client credentials (client_secret) to be stored > as plain text on the client's filesystem, which could also be inadvertently > checked into source control system. > Due to these and other security considerations, we should be able to use > assertions as client credentials for authenticating against the token > endpoint. > While using assertions an oidc client will include client_assertion and > client_assertion_type parameters instead of passing the client_secret for > authentication. > Details on the OAuth 2.0 specifications for client authentication using jwt > and assertions can be found under > # [RFC 7523, Section 2.2 (Using JWTs for Client > Authentication)|https://tools.ietf.org/html/rfc7523#section-2.2] > # [RFC 7521 (Using Assertions for Client > Authentication)|https://tools.ietf.org/html/rfc7521#section-4.2]. > Summary of the changes needed for assertion based client authentication. > * Generate a pair of private and public key which can be made available to > Nifi via a keystore on the filesystem. > * Make the public key or cert available to the Authorization Server. > Alternatively to allow key rotations, we could configure a JWKS URL within > Nifi to allow the authorization server download the public key using the > keyId (kid). > * While building the token request against the token endpoint, need > StandardOidcIdentityProvider to provide a way to build a private_key_jwt > based client authentication in addition to the existing client_secret_basic, > client_secret_post. > {code:java} > if > (ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(clientAuthenticationMethod)){ > > clientAuthentication = new PrivateKeyJWT(clientId, tokenEndpoint, > jwsAlgorithm, privateKey, keyId, null); > }else if > (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthenticationMethod)) > { > clientAuthentication = new ClientSecretPost(clientId, clientSecret); > } else { > clientAuthentication = new ClientSecretBasic(clientId, clientSecret); > }{code} > * Encode and sign the JWT token with the private key, conforming to RFC > 7523, section 2.2 and pass the signed token within the client_assertion > request parameter along with the client_assertion_type. > {code:java} > POST https://<auth-host>:<port>/.../token HTTP/1.1 > Content-Type: application/x-www-form-urlencoded > client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer& > client_assertion=eyJraWQiOiJkMlY2LS1OUG....hTMfA& > grant_type=authorization_code& > code=9ba8d85b-...-c7a6fb6dae55 > {code} > * The Authorization Server will use the client's public key or cert to > verify the signature of the JWT and issues the token > > Nifi Token Exchange Process > When authentication within Nifi is enabled via OpenId Connect, a user is > re-directed to the configured authorization endpoint (authorization server) > via Nifi's oidc/request endpoint. The user upon successfully authenticating > with the authorization server is directed back to Nifi's (oidc/callback > endpoint) with a temporary authorization code. This authorization code is > then exchanged for an ID Token by the OidcService via the Oidc Identity > Provider (StandardOidcIdentityProvider - > exchangeAuthorizationCode(authorizationGrant)). The identity provider > requests this exchange by authenticating itself against the token endpoint of > the authorization server and presenting the authorization grant. The > authorization server authenticates the client, validates the authorization > grant and upon successful validation it issues an access token to Nifi. > Currently, the exchangeAuthorizationCode(...) on StandardOidcIdentityProvider > provides two methods of Client Authentication (client_secret_basic, > client_secret_post) which are both based on the Authorization Server issuing > a set of client credentials (client_secret) which are presented to the > authorization server as client credentials during token exchange. > > {code:java} > public String exchangeAuthorizationCode(final AuthorizationGrant > authorizationGrant) throws IOException {public String > exchangeAuthorizationCode(final AuthorizationGrant authorizationGrant) throws > IOException { ... > // 1 - Build the client authentication using the clientId and clientSecret > obtained during the registration of the client with the authorization server > if > (oidcProviderMetadata.getTokenEndpointAuthMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_POST)) > { clientAuthentication = new ClientSecretPost(clientId, clientSecret); } > else { clientAuthentication = new ClientSecretBasic(clientId, clientSecret); } > // 2 - Build the token endpoint request with the above client authentication > TokenRequest request = new TokenRequest(tokenEndpoint, clientAuthentication, > authorizationGrant); HTTPRequest tokenHttpRequest = request.toHTTPRequest(); > ... > // 3 - Submit the token request and Get the token response TokenResponse > response = OIDCTokenResponseParser.parse(tokenHttpRequest.send()); > // 4 - Upon Successful Response get the ID Token and the User Identity if > (response.indicatesSuccess()) { final OIDCTokenResponse oidcTokenResponse = > (OIDCTokenResponse) response; final OIDCTokens oidcTokens = > oidcTokenResponse.getOIDCTokens(); // validate the access token and parse > the claims final IDTokenClaimsSet claimsSet = > tokenValidator.validate(oidcJwt, null); // attempt to extract the configured > claim to access the user's identity; default is 'email' String identity = > claimsSet.getStringClaim(properties.getOidcClaimIdentifyingUser()); ... } // > 5 - If User Identity could not extracted out of the IDToken, lookup the > identity against the UserInfo endpoint if (StringUtils.isBlank(identity)) { > ... final BearerAccessToken bearerAccessToken = > oidcTokens.getBearerAccessToken(); ... // 7 - Invoke the UserInfo endpoint > to lookup user identity against the UserInfo endpoint identity = > lookupIdentityInUserInfoModified(bearerAccessToken); ... } ... // 8 - > Convert into a Nifi jwt for retrieval later final LoginAuthenticationToken > loginToken = new LoginAuthenticationToken(identity, identity, expiresIn, > claimsSet.getIssuer().getValue()); return > jwtService.generateSignedToken(loginToken);} > {code} > -- This message was sent by Atlassian Jira (v8.3.4#803005)