diff --git a/agama/engine/src/main/java/io/jans/agama/engine/service/ActionService.java b/agama/engine/src/main/java/io/jans/agama/engine/service/ActionService.java index 3f6d2e2adaa..f91b6afa1a1 100644 --- a/agama/engine/src/main/java/io/jans/agama/engine/service/ActionService.java +++ b/agama/engine/src/main/java/io/jans/agama/engine/service/ActionService.java @@ -83,7 +83,7 @@ public Object callAction(Object instance, String className, String methodName, O if (actionCls == null) throw new ClassNotFoundException(rex.getMessage(), rex); } } - logger.info("Class {} loaded successfully", className); + logger.debug("Class {} loaded successfully", className); int arity = rhinoArgs.length; BiPredicate pr = (e, staticRequired) -> { diff --git a/agama/inboundID/src/main/java/io/jans/inbound/JwtUtil.java b/agama/inboundID/src/main/java/io/jans/inbound/JwtUtil.java new file mode 100644 index 00000000000..28c04821999 --- /dev/null +++ b/agama/inboundID/src/main/java/io/jans/inbound/JwtUtil.java @@ -0,0 +1,72 @@ +package io.jans.inbound; + +import com.nimbusds.jose.*; +import com.nimbusds.jose.crypto.*; +import com.nimbusds.jose.jwk.*; +import com.nimbusds.jwt.*; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.spec.EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; + +import java.text.ParseException; +import java.util.Base64; +import java.util.Date; +import java.util.Optional; +import java.util.Map; + +public class JwtUtil { + + private static final Base64.Decoder decoder = Base64.getDecoder(); + + //ECDSA using P-256 (secp256r1) curve and SHA-256 hash algorithm + public static String mkES256SignedJWT(String privateKeyPEM, String kid, String iss, String aud, String sub, int expGap) + throws JOSEException, NoSuchAlgorithmException, InvalidKeySpecException { + + byte[] keyData = decoder.decode(privateKeyPEM); + EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(keyData); + KeyFactory kf = KeyFactory.getInstance("EC"); + PrivateKey privKey = kf.generatePrivate(privKeySpec); + + JWSSigner signer = new ECDSASigner(privKey, Curve.P_256); + long now = System.currentTimeMillis(); + + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .issuer(iss) + .issueTime(new Date(now)) + .expirationTime(new Date(now + expGap * 1000L)) + .audience(aud) + .subject(sub) + .build(); + + SignedJWT signedJWT = new SignedJWT( + new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(kid).type(JOSEObjectType.JWT).build(), + claimsSet); + signedJWT.sign(signer); + return signedJWT.serialize(); + + } + + public static Map partialVerifyJWT(String jwt, String iss, String aud) + throws ParseException, JOSEException { + + JWTClaimsSet claims = SignedJWT.parse(jwt).getJWTClaimsSet(); + + //Apply some validations + if (!iss.equals(claims.getIssuer())) throw new JOSEException("Unexpected issuer value in id_token"); + + if (claims.getAudience().stream().filter(aud::equals).findFirst().isEmpty()) + throw new JOSEException("id_token does not contain the expected audience " + aud); + + long now = System.currentTimeMillis(); + if (Optional.ofNullable(claims.getExpirationTime()).map(Date::getTime).orElse(0L) < now) + throw new JOSEException("Expired id_token"); + + return claims.toJSONObject(); + + } + +} diff --git a/agama/inboundID/src/main/java/io/jans/inbound/Mappings.java b/agama/inboundID/src/main/java/io/jans/inbound/Mappings.java index 413c79791d4..2863cfa6163 100644 --- a/agama/inboundID/src/main/java/io/jans/inbound/Mappings.java +++ b/agama/inboundID/src/main/java/io/jans/inbound/Mappings.java @@ -48,8 +48,6 @@ public final class Mappings { map.put(Attrs.UID, "apple-" + profile.get("sub")); map.put(Attrs.MAIL, profile.get("email")); - map.put(Attrs.DISPLAY_NAME, profile.get("name")); - map.put(Attrs.GIVEN_NAME, profile.get("name")); return map; }; diff --git a/agama/inboundID/src/main/java/io/jans/inbound/oauth2/CodeGrantUtil.java b/agama/inboundID/src/main/java/io/jans/inbound/oauth2/CodeGrantUtil.java index 84bc09029f0..3ac4f200c9e 100644 --- a/agama/inboundID/src/main/java/io/jans/inbound/oauth2/CodeGrantUtil.java +++ b/agama/inboundID/src/main/java/io/jans/inbound/oauth2/CodeGrantUtil.java @@ -64,7 +64,7 @@ public String parseCode(Map urlParams, String state) if (!response.indicatesSuccess()) { // The request was denied or some error occurred AuthorizationErrorResponse errorResponse = response.toErrorResponse(); - throw new GeneralException(errorResponse.getErrorObject().getDescription()); + throw exFromError(errorResponse.getErrorObject()); } return response.toSuccessResponse().getAuthorizationCode().getValue(); @@ -86,14 +86,25 @@ public Map getTokenResponse(String authzCode) if (p.getCustParamsTokenReq() != null) { p.getCustParamsTokenReq().forEach((k, v) -> params.put(k, Collections.singletonList(v))); } + + TokenRequest request; + if (p.isClientCredsInRequestBody()) { + params.put("client_id", Collections.singletonList(p.getClientId())); + params.put("client_secret", Collections.singletonList(p.getClientSecret())); + + request = new TokenRequest(tokenEndpoint, clientID, codeGrant, null, null, null, params); + } else { + request = new TokenRequest(tokenEndpoint, clientAuth, codeGrant, null, null, params); + } - TokenRequest request = new TokenRequest(tokenEndpoint, clientAuth, codeGrant, null, null, params); HTTPRequest httpRequest = request.toHTTPRequest(); httpRequest.setAccept(MediaType.APPLICATION_JSON); + //httpRequest.setContentType("application/x-www-form-urlencoded"); + //httpRequest.setHeader("User-Agent", "curl"); TokenResponse response = TokenResponse.parse(httpRequest.send()); if (!response.indicatesSuccess()) { - throw new GeneralException(response.toErrorResponse().getErrorObject().getDescription()); + throw exFromError(response.toErrorResponse().getErrorObject()); } return response.toSuccessResponse().toJSONObject(); @@ -120,4 +131,26 @@ private MultivaluedMap toMultivaluedMap(Map map) } + private static GeneralException exFromError(ErrorObject o) { + + Map map = new HashMap<>(); + + String s = "" + o.getHTTPStatusCode(); + map.put("HTTP status", s); + + s = o.getCode(); + if (s != null) { + map.put("error code", s); + } + + s = o.getDescription(); + if (s != null) { + map.put("description", s); + } + + s = map.toString(); + return new GeneralException(s.substring(1, s.length() - 1)); + + } + } diff --git a/agama/inboundID/src/main/java/io/jans/inbound/oauth2/OAuthParams.java b/agama/inboundID/src/main/java/io/jans/inbound/oauth2/OAuthParams.java index c3271d90532..8128ae9343d 100644 --- a/agama/inboundID/src/main/java/io/jans/inbound/oauth2/OAuthParams.java +++ b/agama/inboundID/src/main/java/io/jans/inbound/oauth2/OAuthParams.java @@ -15,6 +15,7 @@ public class OAuthParams { private String redirectUri; + private boolean clientCredsInRequestBody; private Map custParamsAuthReq; private Map custParamsTokenReq; @@ -74,6 +75,14 @@ public void setRedirectUri(String redirectUri) { this.redirectUri = redirectUri; } + public boolean isClientCredsInRequestBody() { + return clientCredsInRequestBody; + } + + public void setClientCredsInRequestBody(boolean clientCredsInRequestBody) { + this.clientCredsInRequestBody = clientCredsInRequestBody; + } + public Map getCustParamsAuthReq() { return custParamsAuthReq; } diff --git a/docs/admin/developer/agama/quick-start.md b/docs/admin/developer/agama/quick-start.md index c2036164ec1..6ceab790613 100644 --- a/docs/admin/developer/agama/quick-start.md +++ b/docs/admin/developer/agama/quick-start.md @@ -96,7 +96,7 @@ cat /root/.config/jans-cli.ini | grep 'jca_client_id' cat /root/.config/jans-cli.ini | grep 'jca_client_secret_enc' ``` -The above contains an encoded secret, to decode it run `/opt/jans/bin/encode -D `. Keep this credentials safe. +The above contains an encoded secret, to decode it run `/opt/jans/bin/encode.py -D `. Keep this credentials safe. Tokens are required to have the right scopes depending on the operation to invoke, the following table summarizes this aspect: @@ -121,8 +121,8 @@ You can extract the token from the (JSON) response obtained which is a self-expl **Notes**: - Tokens have expiration time measured in seconds. When expired, you'll have to re-request -- To get a token with more than one scope, supply the required scopes separated by whitespace in the `scope` parameter of the request above -- You don't have to necessarily use the jans-config-api client to get your tokens. Any client, including one registered yourself can be used here as long as it provides the needed scopes +- To get a token with more than one scope, supply the required scopes separated by whitespace in the `scope` parameter of the request above +- You don't have to necessarily use the jans-config-api client to get your tokens. Any client, including one registered yourself can be used here as long as it provides the needed scopes ### Add the flow to the server diff --git a/docs/script-catalog/agama/inboundID/README.md b/docs/script-catalog/agama/inboundID/README.md index 122b39db6a6..cb5d93609bc 100644 --- a/docs/script-catalog/agama/inboundID/README.md +++ b/docs/script-catalog/agama/inboundID/README.md @@ -231,6 +231,7 @@ The table below explains the meaning of its properties: |`clientSecret`|Secret associated to the client| |`scopes`|A JSON array of strings that represent the scopes of the access tokens to retrieve| |`redirectUri`|Redirect URI as in section 3.1.2 of [RFC 7649](https://www.ietf.org/rfc/rfc6749)| +|`clientCredsInRequestBody`|`true` indicates the client authenticates at the token endpoint by including the credentials in the body of the request, otherwise, HTTP Basic authentication is assumed. See section 2.3.1 of [RFC 7649](https://www.ietf.org/rfc/rfc6749)| |`custParamsAuthReq`|A JSON object (keys and values expected to be strings) with extra parameters to pass to the authorization endpoint if desired| |`custParamsTokenReq`|A JSON object (keys and values expected to be strings) with extra parameters to pass to the token endpoint if desired| diff --git a/docs/script-catalog/agama/inboundID/apple/apple.png b/docs/script-catalog/agama/inboundID/apple/apple.png new file mode 100644 index 00000000000..d852c10925f Binary files /dev/null and b/docs/script-catalog/agama/inboundID/apple/apple.png differ diff --git a/docs/script-catalog/agama/inboundID/apple/io.jans.inbound.Apple b/docs/script-catalog/agama/inboundID/apple/io.jans.inbound.Apple new file mode 100644 index 00000000000..6029513cbf5 --- /dev/null +++ b/docs/script-catalog/agama/inboundID/apple/io.jans.inbound.Apple @@ -0,0 +1,18 @@ +Flow io.jans.inbound.Apple + Basepath "" + Configs p + +issuer = "https://appleid.apple.com" +//See https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#create-the-client-secret +p.clientSecret = Call io.jans.inbound.JwtUtil#mkES256SignedJWT p.key p.keyId p.teamId issuer p.clientId 60 + +obj = Trigger io.jans.inbound.oauth2.AuthzCode p +When obj.success is false + Finish obj + +//See https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user#verify-the-identity-token +claims = Call io.jans.inbound.JwtUtil#partialVerifyJWT obj.data.id_token issuer p.clientId + +//Most claims don't carry profile data, e.g. iss, iat, exp, ... +obj = { success: true, data: { sub: claims.sub, email: claims.email } } +Finish obj diff --git a/jans-auth-server/server/conf/jans-config.json b/jans-auth-server/server/conf/jans-config.json index 4472bcc032a..9768663f93e 100644 --- a/jans-auth-server/server/conf/jans-config.json +++ b/jans-auth-server/server/conf/jans-config.json @@ -430,7 +430,7 @@ "templatesPath": "/ftl", "scriptsPath": "/scripts", "serializerType": "KRYO", - "maxItemsLoggedInCollections": 3, + "maxItemsLoggedInCollections": 7, "pageMismatchErrorPage": "mismatch.ftlh", "interruptionErrorPage": "timeout.ftlh", "crashErrorPage": "crash.ftlh", diff --git a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json index fe9fd433c4c..8742dba3c8d 100644 --- a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json +++ b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json @@ -461,7 +461,7 @@ "templatesPath": "/ftl", "scriptsPath": "/scripts", "serializerType": "KRYO", - "maxItemsLoggedInCollections": 3, + "maxItemsLoggedInCollections": 9, "pageMismatchErrorPage": "mismatch.ftlh", "interruptionErrorPage": "timeout.ftlh", "crashErrorPage": "crash.ftlh",