Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(agama): add utility classes for inbound identity #2280

Merged
merged 3 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<Executable, Boolean> pr = (e, staticRequired) -> {
Expand Down
72 changes: 72 additions & 0 deletions agama/inboundID/src/main/java/io/jans/inbound/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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();

}

}
2 changes: 0 additions & 2 deletions agama/inboundID/src/main/java/io/jans/inbound/Mappings.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public String parseCode(Map<String, Object> 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();
Expand All @@ -86,14 +86,25 @@ public Map<String, Object> 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();

Expand All @@ -120,4 +131,26 @@ private MultivaluedMap<String, String> toMultivaluedMap(Map<String, Object> map)

}

private static GeneralException exFromError(ErrorObject o) {

Map<String, String> 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));

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class OAuthParams {

private String redirectUri;

private boolean clientCredsInRequestBody;
private Map<String, String> custParamsAuthReq;
private Map<String, String> custParamsTokenReq;

Expand Down Expand Up @@ -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<String, String> getCustParamsAuthReq() {
return custParamsAuthReq;
}
Expand Down
6 changes: 3 additions & 3 deletions docs/admin/developer/agama/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <DECODED-PASS>`. Keep this credentials safe.
The above contains an encoded secret, to decode it run `/opt/jans/bin/encode.py -D <DECODED-PASS>`. Keep this credentials safe.

Tokens are required to have the right scopes depending on the operation to invoke, the following table summarizes this aspect:

Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/script-catalog/agama/inboundID/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions docs/script-catalog/agama/inboundID/apple/io.jans.inbound.Apple
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion jans-auth-server/server/conf/jans-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@
"templatesPath": "/ftl",
"scriptsPath": "/scripts",
"serializerType": "KRYO",
"maxItemsLoggedInCollections": 3,
"maxItemsLoggedInCollections": 7,
"pageMismatchErrorPage": "mismatch.ftlh",
"interruptionErrorPage": "timeout.ftlh",
"crashErrorPage": "crash.ftlh",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@
"templatesPath": "/ftl",
"scriptsPath": "/scripts",
"serializerType": "KRYO",
"maxItemsLoggedInCollections": 3,
"maxItemsLoggedInCollections": 9,
"pageMismatchErrorPage": "mismatch.ftlh",
"interruptionErrorPage": "timeout.ftlh",
"crashErrorPage": "crash.ftlh",
Expand Down