Skip to content

Commit

Permalink
feat(agama): add utility classes for inbound identity (#2280)
Browse files Browse the repository at this point in the history
* docs: add new property to table #2197

* feat: allow client_secret_post authn method for token endpoint #2197

* feat: add SIWA flow #2198
  • Loading branch information
jgomer2001 committed Aug 31, 2022
1 parent cea10ff commit ca6fdc9
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 11 deletions.
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

0 comments on commit ca6fdc9

Please sign in to comment.