![JAR search and dependency download from the Maven repository](/logo.png)
org.id4me.Id4meLogon Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of relying-party-api Show documentation
Show all versions of relying-party-api Show documentation
ID4me RelyingParty API prototype
The newest version!
/*
* Copyright (C) 2016-2020 OX Software GmbH
* Developed by Peter Höbel [email protected]
* See the LICENSE file for licensing conditions
* SPDX-License-Identifier: MIT
*/
package org.id4me;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import java.util.Hashtable;
import java.util.Properties;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.id4me.Id4meResolver.Id4meDnsDataWithLoginHint;
import org.id4me.config.Id4meClaimsParameters;
import org.id4me.config.Id4meProperties;
import org.id4me.exceptions.ClientNotRegisteredException;
import org.id4me.exceptions.ClientRegistrationSecretExpiredException;
import org.id4me.exceptions.MandatoryClaimsException;
import org.id4me.exceptions.TokenNotFoundException;
import org.id4me.exceptions.TokenValidationException;
import org.id4me.util.FileReader;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSADecrypter;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.util.BoundedInputStream;
import com.nimbusds.jwt.EncryptedJWT;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.SignedJWT;
/**
* Id4meLogon provides all functionality used to logon a user at a Id4ME
* identity authority and query the userinfo from a Id4ME identity agent.
*
* Any information about the session state is contained by an instance of
* {@link Id4meSessionData}
*
* @author Peter Hoebel
*
*/
public class Id4meLogon {
private static final Logger log = LoggerFactory.getLogger(Id4meLogon.class);
private static final String UTF_8 = StandardCharsets.UTF_8.name();
private static final String CONTENT_TYPE = "Content-Type";
private static final String AUTHORIZATION = "Authorization";
private int MAX_FETCH_SIZE = 50000;
private Id4meIdentityAuthorityStorage storage = Id4meIdentityAuthorityStorage2.INSTANCE;
private Id4meResolver resolver;
private String clientName;
private String redirectUri;
private String[] redirectUris;
private String logoUri;
private Path registrationDataPath;
private final Id4meClaimsConfig claimsConfig;
private Id4meKeyPairHandler keyPairHandler;
private boolean fallbackToScopes;
/**
* Read the configuration files and initialize the local variables needed to
* perform an ID4me logon flow.
*
* @param id4MePropertiesFile contains the properties needed for the client
* registration
* @param claimsParametersFile contains the claims configuration in json format
* @throws Exception on any error
*/
public Id4meLogon(String id4MePropertiesFile, String claimsParametersFile) throws Exception {
readPropertiesFile(id4MePropertiesFile);
claimsConfig = readClaimsParametersFile(claimsParametersFile);
initSSLSocketFactory();
}
/**
* Return the ID4me claims configuration which was created in the
* {@link Id4meLogon} constructor
*
* @return the claims claims configuration
*/
public Id4meClaimsConfig getClaimsConfig() {
return claimsConfig;
}
/**
* Constructs the {@link Id4meLogon} with external configuration.
*
* @param id4meProperties contains the properties needed for the client
* registration
* @param claimsParameters contains the claims configuration
* @throws Exception on any error
*/
public Id4meLogon(Id4meProperties id4meProperties, Id4meClaimsParameters claimsParameters) throws Exception {
readProperties(id4meProperties);
claimsConfig = new Id4meClaimsConfig(claimsParameters);
initSSLSocketFactory();
}
public Id4meLogon(Id4meProperties id4meProperties, Id4meClaimsParameters claimsParameters, String[] scopes)
throws Exception {
readProperties(id4meProperties);
claimsConfig = new Id4meClaimsConfig(claimsParameters);
if (scopes != null)
for (String scope : scopes) {
claimsConfig.addScope(scope);
}
initSSLSocketFactory();
}
public void setIdentityAuthroityStorage(Id4meIdentityAuthorityStorage storage) {
this.storage = storage;
}
private void initSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException {
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
// Do nothing - the client is trusted if no exception is thrown
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
// Do nothing - the server is trusted if no exception is thrown
}
} };
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}
private void readPropertiesFile(String filename) throws Exception {
Path path = Paths.get(filename);
// Not using Files.exists(..) because of poor performance in JDK 8
if (!path.toFile().exists()) {
throw new Exception("Properties file " + path + " not found!");
}
Properties props = new Properties();
try (InputStream in = Files.newInputStream(path)) {
props.load(in);
}
if (props.containsKey("client.max_fetch_size")) {
try {
String mx = props.getProperty("client.max_fetch_size");
this.MAX_FETCH_SIZE = Integer.parseInt(mx);
} catch (Exception ex) {
log.error("Error determining MAX_FETCH_SIZE from properties: " + ex.getMessage());
}
}
if (props.containsKey("client.name")) {
this.clientName = props.getProperty("client.name");
} else {
throw new Exception("property client.name not found in " + path);
}
if (props.containsKey("redirect.uri")) {
this.redirectUri = props.getProperty("redirect.uri");
} else {
throw new Exception("property redirect.uri not found in " + path);
}
if (props.containsKey("redirect.uris")) {
this.redirectUris = props.getProperty("redirect.uris").split(",");
} else {
this.redirectUris = new String[] { this.redirectUri };
}
if (props.containsKey("logo.uri")) {
this.logoUri = props.getProperty("logo.uri");
}
String rootKey;
if (props.containsKey("dnssec_root_key")) {
rootKey = props.getProperty("dnssec_root_key");
}
// Also check the old parameter name that contained a typo (three 's')
else if (props.containsKey("dnsssec_root_key")) {
rootKey = props.getProperty("dnsssec_root_key");
} else {
rootKey = ". IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D";
}
boolean dnssecRequired = true;
if (props.containsKey("dnssec_required")) {
dnssecRequired = Boolean.parseBoolean(props.getProperty("dnssec_required"));
}
String dnsResolver;
if (props.containsKey("dns.resolver")) {
dnsResolver = props.getProperty("dns.resolver");
} else {
dnsResolver = "127.0.0.1";
}
this.resolver = new Id4meResolver(dnsResolver, rootKey, dnssecRequired);
if (props.containsKey("registration.data.path")) {
registrationDataPath = Paths.get(props.getProperty("registration.data.path", null));
} else {
registrationDataPath = Paths.get("./");
}
if (props.containsKey("pub_key") && props.containsKey("priv_key")) {
keyPairHandler = new Id4meKeyPairHandler(props.getProperty("pub_key"), props.getProperty("priv_key"));
}
if (props.containsKey("scopes_fallback")) {
fallbackToScopes = Boolean.parseBoolean(props.getProperty("scopes_fallback"));
}
// rootKey and dnsResolver are passed to logProperties(..) as they are not
// stored in fields
logProperties(rootKey, dnsResolver);
}
private void readProperties(Id4meProperties properties) throws Exception {
if (properties.getMaxFetchSize() != null) {
this.MAX_FETCH_SIZE = properties.getMaxFetchSize();
}
if (properties.getClientName() != null) {
this.clientName = properties.getClientName();
} else {
throw new Exception("Id4meProperties.clientName not set");
}
if (properties.getRedirectURI() != null) {
this.redirectUri = properties.getRedirectURI();
} else {
throw new Exception("Id4meProperties.redirectURI not set");
}
if (properties.getRedirectURIs() != null) {
this.redirectUris = properties.getRedirectURIs();
} else {
this.redirectUris = new String[] { this.redirectUri };
}
if (properties.getLogoURI() != null) {
this.logoUri = properties.getLogoURI();
}
String rootKey;
if (properties.getDnssecRootKey() != null) {
rootKey = properties.getDnssecRootKey();
} else {
rootKey = ". IN DS 19036 8 2 49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5";
}
String dnsResolver;
if (properties.getDnsResolver() != null) {
dnsResolver = properties.getDnsResolver();
} else {
dnsResolver = "127.0.0.1";
}
this.resolver = new Id4meResolver(dnsResolver, rootKey, properties.isDnssecRequired());
if (properties.getRegistrationDataPath() != null) {
String path = properties.getRegistrationDataPath();
registrationDataPath = Paths.get(path);
} else {
registrationDataPath = Paths.get("./");
}
if (properties.getPrivKeyFile() != null && properties.getPubKeyFile() != null) {
keyPairHandler = new Id4meKeyPairHandler(properties.getPubKeyFile(), properties.getPrivKeyFile());
}
fallbackToScopes = properties.isFallbackToScopes();
// rootKey and dnsResolver are passed to logProperties(..) as they are not
// stored in fields
logProperties(rootKey, dnsResolver);
}
private void logProperties(String rootKey, String dnsResolver) {
log.info("Configured client name: {}", clientName);
log.info("Configured redirect URI: {}", redirectUri);
log.info("Configured redirect URIs: {}", Arrays.toString(redirectUris));
log.info("Configured logo URI: {}", logoUri);
log.info("Configured DNSSEC root key: {}", rootKey);
log.info("Configured DNS resolver: {}", dnsResolver);
log.info("Configured registration data path: {}", registrationDataPath);
if (keyPairHandler != null) {
log.info("Configured keyPairHandler: {}", keyPairHandler.toString());
}
}
private static String readFile(String filename) throws Exception {
Path path = Paths.get(filename);
if (Files.exists(path)) {
return FileReader.readFileToString(path);
}
throw new Exception("File not found: " + path);
}
private Id4meClaimsConfig readClaimsParametersFile(String filename) throws Exception {
Path path = Paths.get(filename);
if (!Files.exists(path)) {
throw new Exception("Claims configuration file " + path.toAbsolutePath() + " not found!");
}
String claimsConfigJSON = readFile(filename);
return Id4meClaimsConfigParser.parseClaimsConfigJSON(claimsConfigJSON);
}
/**
* Create a new {@link Id4meSessionData} object for a give id4me. If the client
* is not already registered at the identity authority the dynamic client
* registration can be done implicitily
*
* @param id4me The id4me for this session.
* @param autoRegisterClient Flag, indicating whether the client shall be
* automatically registered, if not already done.
* @return Id4meSessionData The session data
* @throws Exception In case of error
*
*/
public Id4meSessionData createSessionData(String id4me, boolean autoRegisterClient) throws Exception {
Id4meSessionData sessionData = new Id4meSessionData();
sessionData.setId4me(id4me);
Id4meDnsDataWithLoginHint dnsDataWithLoginHint = resolver.getDataFromDns(id4me);
Id4meDnsData dnsData = dnsDataWithLoginHint.getDnsResponse();
sessionData.setLoginHint(dnsDataWithLoginHint.getLoginHint());
sessionData.setIau(dnsData.getIau());
sessionData.setIag(dnsData.getIag());
sessionData.setRedirectUri(URLEncoder.encode(redirectUri, UTF_8));
sessionData.setLogoUri(URLEncoder.encode(logoUri, UTF_8));
log.debug("Creating session data using login hint: {}", dnsDataWithLoginHint.getLoginHint());
log.debug("Creating session data using redirect URI: {}", redirectUri);
log.debug("Creating session data using logo URI: {}", logoUri);
getIauData(sessionData, autoRegisterClient);
return sessionData;
}
/**
* Unsubscribes the relying party at the identity authority and removes the
* registration data from the local storage.
*
* @param sessionData {@link Id4meSessionData} the current Id4ME session
* object
* @return true if the http response code >= 200 && <300,
* otherwise false
*/
public boolean unsubscribeIau(Id4meSessionData sessionData) {
String iau = sessionData.getIau();
JSONObject registration = sessionData.getIauData().getRegistrationData();
String registrationAccessToken = registration.getString("registration_access_token");
String registrationClientUri = registration.getString("registration_client_uri");
String authHeader = buildAuthHeader(registrationAccessToken);
log.info("Unsubscribing IAU with registrationClientUri: {}", registrationClientUri);
try {
URL url = new URL(registrationClientUri);
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
con.setInstanceFollowRedirects(true);
// add request header
con.setRequestMethod("DELETE");
con.setRequestProperty(CONTENT_TYPE, "application/json");
con.setRequestProperty(AUTHORIZATION, authHeader);
con.setDoOutput(true);
try (DataOutputStream wr = new DataOutputStream(con.getOutputStream())) {
wr.writeBytes("{}");
wr.flush();
}
int responseCode = con.getResponseCode();
log.info("Unsubscribing IAU response code: {}", responseCode);
if (responseCode >= 200 && responseCode < 300) {
StringBuilder response = new StringBuilder();
try (BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
}
log.info("Unsubscribing IAU response: {}", response);
storage.removeIauData(registrationDataPath, iau);
return true;
} else {
return false;
}
} catch (Exception ex) {
log.error("Error unsubscribing IAU", ex.getMessage());
return false;
}
}
private JSONObject fetchJwtsData(Id4meSessionData sessionData) throws Exception {
String jwtUri = sessionData.getIauData().getWellKnown().getString("jwks_uri");
String jwts = fetchUrl(jwtUri);
return new JSONObject(jwts);
}
/**
* Do a dynamic client registration for the relying party at the InetId's
* identity authority and save the registration data in the local registration
* storage.
*
* @param sessionData {@link Id4meSessionData} the current Id4ME session
* object
* @throws Exception if getRegistrationData or storage.saveRegistrationData
* throws any
*/
public void doDynamicClientRegistration(Id4meSessionData sessionData) throws Exception {
String iau = sessionData.getIau();
log.info("Trying dynamic client registration for IAU: {}", iau);
Id4meIdentityAuthorityData data = sessionData.getIauData();
if (data == null) {
throw new Exception("No iau data found in session for dynamic client registration!");
}
// save the .well-known data
JSONObject well_known = data.getWellKnown();
if (well_known == null) {
throw new Exception("well-known data not found in session for dynamic client registration!");
}
JSONObject registrationData = getRegistrationData(sessionData);
data = storage.saveRegistrationData(registrationDataPath, iau, registrationData);
// restore the .well-known data
data.setWellKnown(well_known);
sessionData.setIauData(data);
return;
}
/**
* Returns the Id4ME authentication request uri.
*
* @param sessionData {@link Id4meSessionData} the current Id4ME session
* object
* @return the authentication request uri to redirect the client to
* @throws UnsupportedEncodingException if the encoding of the parameter claims
* fails
*/
public String authorize(Id4meSessionData sessionData) throws UnsupportedEncodingException {
Id4meIdentityAuthorityData data = sessionData.getIauData();
JSONObject wellKnown = sessionData.getIauData().getWellKnown();
String claimsParam = "";
String scopeParam = "&scope=" + claimsConfig.getScopesParam();
if (wellKnown.has("claims_parameter_supported")) {
Boolean claims_parameter_supported = wellKnown.getBoolean("claims_parameter_supported");
if (claims_parameter_supported) {
log.debug("Claims parameter are supported.");
// String info = "{ \"userinfo\": { \"verified_claims\": { \"claims\": { \"family_name\": { \"purpose\": \"\", \"essential\": true }, \"given_name\": { \"purpose\": \"\", \"essential\": true } }, \"verification\": { \"trust_framework\": { \"value\": \"denic_framework\" } } } } }";
// claimsParam = "&claims=" + info;
claimsParam = "&claims=" + claimsConfig.getClaimsParam();
} else {
log.debug("Claims parameter are not supported.");
if (fallbackToScopes) {
scopeParam = "&scope=" + claimsConfig.getScopesForClaims();
log.debug("claims_parameter_supported == false AND fallbackToScops == true, add missing scopes for "
+ data.getIau());
log.info(data.getIau() + ", set new scopeParam: " + scopeParam);
} else {
log.debug("Claims parameter not supported for " + data.getIau() + ", fallback to scopes == false.");
}
}
} else {
claimsParam = "&claims=" + claimsConfig.getClaimsParam();
log.debug("claims_parameter_supported not found in .wellKnown data for " + data.getIau()
+ ", scopes not modified");
}
String authorizeUri = data.getWellKnown().getString("authorization_endpoint") + "?" + "response_type=code"
+ claimsParam + scopeParam + "&client_id=" + sessionData.getIauData().getClientId() + "&redirect_uri="
+ sessionData.getRedirectUri() + "&state=" + sessionData.getState() + "&nonce=" + sessionData.getNonce()
+ "&login_hint=" + sessionData.getLoginHint();
log.info("Authorizing: authorize URI: {}", authorizeUri);
return authorizeUri;
}
/**
* Gets the access-token from the identity authority and stores it into
* {@link Id4meSessionData} If the id token is encrypted a
* {@link Id4meKeyPairHandler} must be provided.
*
* @param sessionData {@link Id4meSessionData} the current Id4ME session
* object
* @param code The code parameter, received by the redirect of the
* {@link Id4meLogon#authorize} call
* @throws TokenNotFoundException if no bearer token is found in the
* access-token or Exception if
* {@link Id4meLogon#getToken(Id4meSessionData, String)}
* throws one.
* @throws Exception on any other error
*/
public void authenticate(Id4meSessionData sessionData, String code) throws Exception {
JSONObject bearerToken = getToken(sessionData, code);
log.info("Authenticating with token: {}", bearerToken);
if (bearerToken.has("token_type")) {
String type = bearerToken.getString("token_type");
if (!type.equalsIgnoreCase("bearer")) {
throw new TokenNotFoundException("Bearer token not found in response!");
}
}
String identityHandle = null;
if (bearerToken.has("id_token")) {
String id_token = bearerToken.getString("id_token");
try {
EncryptedJWT jwt = EncryptedJWT.parse(id_token);
if (jwt.getIV() != null) {
// id token seem to be encrypted
if (keyPairHandler != null) {
RSADecrypter decrypter = new RSADecrypter(keyPairHandler.getKeyPair().getPrivate());
jwt.decrypt(decrypter);
id_token = jwt.getPayload().toString();
} else {
throw new Exception("id token seem to be encrypted but no KeyPair found!");
}
}
} catch (ParseException ex) {
// id token may not encrypted
log.debug(ex.toString());
}
sessionData.setIdToken(id_token);
if (identityHandle == null)
identityHandle = identityHandleFromIdToken(sessionData, id_token);
}
if (bearerToken.has("access_token")) {
String access_token = bearerToken.getString("access_token");
sessionData.setAccessToken(access_token);
if (identityHandle == null)
identityHandle = identityHandleFromAccessToken(sessionData, access_token);
}
if (identityHandle != null)
sessionData.setIdentityHandle(identityHandle);
sessionData.setBearerToken(bearerToken);
sessionData.setUserinfo(sessionData.getAccessTokenUserinfo());
if (sessionData.getUserinfo() == null)
sessionData.setUserinfo(sessionData.getIdTokenUserinfo());
if (getExpired(bearerToken) != null)
sessionData.setTokenExpires(getExpired(bearerToken));
return;
}
public Long getExpired(JSONObject token) {
if (token.has("expires_in")) {
long exp = token.getLong("expires_in");
long expIn = System.currentTimeMillis() + exp * 1000;
log.debug("Authenticate: Set token to expire in {} sec", exp);
return expIn;
}
return null;
}
/**
* Parse and validate an Access Token and extract the Id4ME Identity
* Handle and its payload
*
* @param sessionData {@link Id4meSessionData} the current Id4ME session
* object
* @param access_token {@link String} the access_token String
* @throws Exception on any other error
* @return String Id4ME Identity Hanlde, or null if not present in the
* token
*/
public String identityHandleFromAccessToken(Id4meSessionData sessionData, String access_token) throws Exception {
log.info("Try to get identity handle from access_token");
String identityHandle = null;
String[] a_fields = access_token.split("\\.");
switch (a_fields.length) {
case 1:
log.debug("AccessToken contains only a header");
break;
case 2:
log.debug("AccessToken contains header and payload");
break;
case 3:
log.debug("AccessToken contains header, payload and signature");
SignedJWT signedToken = (SignedJWT) JWTParser.parse(access_token);
JSONObject jwtsData = fetchJwtsData(sessionData);
validateSignedToken(jwtsData, signedToken);
JSONObject userinfo = new JSONObject(signedToken.getPayload().toString());
sessionData.setAccessTokenUserinfo(userinfo);
if (userinfo.has("iss") && userinfo.has("sub")) {
identityHandle = userinfo.getString("iss") + "#" + userinfo.getString("sub");
}
break;
default:
}
return identityHandle;
}
/**
* Parse and validate an Id Token and extract the Id4ME Identity Handle
* and its payload
*
* @param sessionData {@link Id4meSessionData} the current Id4ME session
* object
* @param id_token {@link String} the id token String
* @throws Exception on any other error
* @return String Id4ME Identity Hanlde, or null if not present in the
* token
*/
public String identityHandleFromIdToken(Id4meSessionData sessionData, String id_token) throws Exception {
String identityHandle = sessionData.getIdentityHandle();
String[] id_fields = id_token.split("\\.");
switch (id_fields.length) {
case 1:
log.info("IdToken contains only a header");
break;
case 2:
log.info("IdToken contains header and payload");
break;
case 3:
log.info("IdToken contains header, payload and signature");
SignedJWT signedToken = (SignedJWT) JWTParser.parse(id_token);
JSONObject jwtsData = fetchJwtsData(sessionData);
validateSignedToken(jwtsData, signedToken);
validateIdTokenPayload(sessionData, signedToken);
JSONObject userinfo = new JSONObject(signedToken.getPayload().toString());
sessionData.setIdTokenUserinfo(userinfo);
if (userinfo.has("iss") && userinfo.has("sub")) {
String identity = userinfo.getString("iss") + "#" + userinfo.getString("sub");
if (identityHandle != null && !identity.equals(identityHandle)) {
throw new Exception("claims sub + iss from access_token and id_token are different!");
} else {
identityHandle = identity;
}
}
break;
default:
}
return identityHandle;
}
/**
* Get the userinfo from the userinfo_endpoint which is in the
* .well-known
data from the identity authority. If the
* userinfo_endpoint is a json object with the members _claim_names and
* _claim_sources, the userinfo gets discovered via the distributed claims
* mechanism.
openid-connect-core-1_0.html#AggregatedDistributedClaims
*
* @param sessionData {@link Id4meSessionData} the current Id4ME session
* object
* @throws MandatoryClaimsException if any mandatory claim is missing or an
* Exception if getUserinfo() throws one
* @throws Exception on any other error
* @return the userinfo object
*/
public JSONObject userinfo(Id4meSessionData sessionData) throws Exception {
JSONObject userinfo = getUserinfo(sessionData);
if (userinfo.has("verified_claims")) {
}
if (userinfo.has("claims")) {
// workaround because of an iag error, move the claims from the claims array to
// the json root element
JSONObject json = new JSONObject();
String[] names = JSONObject.getNames(userinfo);
for (String n : names) {
if (!n.equals("claims")) {
json.put(n, userinfo.get(n));
}
}
JSONObject claims = userinfo.getJSONObject("claims");
names = JSONObject.getNames(claims);
for (String n : names) {
json.put(n, claims.get(n));
}
userinfo = json;
}
checkMandatoryClaims(userinfo);
sessionData.setUserinfo(userinfo);
sessionData.setState("userinfo");
return userinfo;
}
private void checkMandatoryClaims(JSONObject userinfo) throws MandatoryClaimsException {
for (String claimName : claimsConfig.getEssentialClaims()) {
if (!userinfo.has(claimName)) {
log.info("Mandatory claim \"{}\" not found in userinfo: {}", claimName, userinfo);
throw new MandatoryClaimsException("Mandatory claim \"" + claimName + "\" not found!");
}
}
}
/**
* Does the dynamic client registration and stores the registration data into as
* JSON string in a text file. This method could be called from a relying party
* if no automatic registration is used.
*
* @param id4me The Id4ME to register the client for
* @return The Id4meSessionData for the registered client
*
* @throws Exception on any error
*
*/
public Id4meSessionData registerClient(String id4me) throws Exception {
Id4meSessionData sessionData = new Id4meSessionData();
Id4meDnsDataWithLoginHint dnsDataWithLoginHint = resolver.getDataFromDns(id4me);
Id4meDnsData dnsData = dnsDataWithLoginHint.getDnsResponse();
sessionData.setLoginHint(dnsDataWithLoginHint.getLoginHint());
sessionData.setIau(dnsData.getIau());
sessionData.setIag(dnsData.getIag());
sessionData.setRedirectUri(URLEncoder.encode(redirectUri, UTF_8));
sessionData.setLogoUri(URLEncoder.encode(logoUri, UTF_8));
synchronized (storage) {
String iau = sessionData.getIau();
Id4meIdentityAuthorityData data = storage.getIauData(registrationDataPath, iau);
String wellKnownUri = "https://" + iau + "/.well-known/openid-configuration";
String wellKnownData = fetchUrl(wellKnownUri);
JSONObject wellKnown = new JSONObject(wellKnownData);
if (data == null) {
data = new Id4meIdentityAuthorityData();
data.setIau(iau);
data.setWellKnown(wellKnown);
sessionData.setIauData(data);
doDynamicClientRegistration(sessionData);
}
}
return sessionData;
}
/**
* Gets the .well-known/openid-configuration for the identitity authority
*
* @param sessionData {@link Id4meSessionData} the current Id4ME
* session object
* @param autoRegisterClient boolean indicating whether the client should do a
* dynamic client registration if it is not already
* done
* @throws Exception if fetchUrl() throws one
*/
private void getIauData(Id4meSessionData sessionData, boolean autoRegisterClient) throws Exception {
String iau = sessionData.getIau();
log.info("Retrieving identity authority: {}", iau);
// this block is called only once from Id4meLogon.createSessionData(), at the
// beginning of the login flow
// later in the flow the here created/read data from the session is re-used
Id4meIdentityAuthorityData data = storage.getIauData(registrationDataPath, iau);
synchronized (storage) {
String wellKnownUri = "https://" + iau + "/.well-known/openid-configuration";
String wellKnownData = fetchUrl(wellKnownUri);
JSONObject wellKnown = new JSONObject(wellKnownData);
log.debug(wellKnown.toString(2));
if (data == null) {
if (autoRegisterClient) {
data = new Id4meIdentityAuthorityData();
data.setWellKnown(wellKnown);
sessionData.setIauData(data);
doDynamicClientRegistration(sessionData);
return;
} else {
throw new ClientNotRegisteredException("Client is not registered at " + iau);
}
} else {
log.info("Identity authority used: {}", iau);
// TODO check the possibility of refreshing the registration instead of creating
// a new one.
JSONObject registration = data.getRegistrationData();
long client_secret_expires_at = registration.getLong("client_secret_expires_at");
if (client_secret_expires_at != 0) {
long NOW = System.currentTimeMillis();
long validity = (client_secret_expires_at * 1000) - NOW;
log.info("client_secret valid for : " + (validity * 0.001) + " seconds.");
if (validity < 600000) {
if (autoRegisterClient) {
storage.removeIauData(registrationDataPath, iau);
data = new Id4meIdentityAuthorityData();
data.setWellKnown(wellKnown);
sessionData.setIauData(data);
doDynamicClientRegistration(sessionData);
return;
} else {
throw new ClientRegistrationSecretExpiredException(
"Client registration secret is is expired at " + iau);
}
}
}
data.setWellKnown(wellKnown);
sessionData.setIauData(data);
}
}
return;
}
private String fetchUrl(String httpsUrl) throws IOException {
URL url = new URL(httpsUrl);
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
con.setInstanceFollowRedirects(true);
log.debug("fetchUrl: " + con.getResponseCode() + ", " + con.getResponseMessage());
InputStream bounded = new BoundedInputStream(con.getInputStream(), MAX_FETCH_SIZE);
StringBuilder response = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(bounded))) {
String input;
while ((input = br.readLine()) != null) {
response.append(input);
}
}
log.debug("Fetched from URL: {}", httpsUrl);
log.debug("Fetched from URL response: {}", response);
return response.toString();
}
/**
* Perform the dynamic client registration. If keyPairHandler
* {@link Id4meKeyPairHandler} != null, encryption of the id token is requested.
*
* @param sessionData {@link Id4meSessionData} the current Id4ME
* session object
* @param autoRegisterClient boolean indicating whether the client should do a
* dynamic client registration if it is not already
* done
* @param logonData
*
* @return the registration data, should be stored persistently.
* @throws Exception if any occurs
*/
private JSONObject getRegistrationData(Id4meSessionData sessionData) throws Exception {
Id4meIdentityAuthorityData data = sessionData.getIauData();
String httpsUrl = data.getWellKnown().getString("registration_endpoint");
URL url = new URL(httpsUrl);
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
con.setInstanceFollowRedirects(true);
// add request header
con.setRequestMethod("POST");
con.setRequestProperty(CONTENT_TYPE, "application/json");
con.setDoOutput(true);
String[] redirectUrisLocal = new String[redirectUris.length];
int i = 0;
for (String uri : redirectUris) {
redirectUrisLocal[i++] = URLDecoder.decode(uri, UTF_8);
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("redirect_uris", redirectUrisLocal);
jsonObject.put("client_name", clientName);
if (logoUri != null && !"".equals(logoUri.trim())) {
jsonObject.put("logo_uri", logoUri);
}
jsonObject.put("application_type", "web");
if (keyPairHandler != null) {
JSONArray id_alg = data.getWellKnown().getJSONArray("id_token_encryption_alg_values_supported");
// test, whether the identity authority supports RSA-OAEP-256, which is the
// default algorithm
if (id_alg.toList().contains("RSA-OAEP-256")) {
// default algorithm is supported
data.getWellKnown().getJSONArray("id_token_encryption_enc_values_supported");
jsonObject.put("id_token_encrypted_response_alg", "RSA-OAEP-256");
JSONObject jwks = keyPairHandler.generateJwks();
jsonObject.put("jwks", jwks);
}
}
String json = jsonObject.toString();
log.info("Registration data request: {}", json);
try (DataOutputStream wr = new DataOutputStream(con.getOutputStream())) {
wr.writeBytes(json);
wr.flush();
}
int responseCode = con.getResponseCode();
String msg = con.getResponseMessage();
log.info("Registration data request response code: {}, message: {}", responseCode, msg);
if (responseCode == 200 || responseCode == 201) {
StringBuilder response = new StringBuilder();
try (BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
}
JSONObject result = new JSONObject(response.toString());
log.info("Registration data:\n{}", result.toString(2));
return result;
} else {
throw new Exception("Error " + responseCode + " on url " + httpsUrl);
}
}
private JSONObject getToken(Id4meSessionData logonData, String code) throws Exception {
Id4meIdentityAuthorityData data = logonData.getIauData();
String httpsUrl = data.getWellKnown().getString("token_endpoint");
String urlParameters = "grant_type=authorization_code&code=" + code + "&redirect_uri="
+ logonData.getRedirectUri() + "&nonce=" + logonData.getNonce();
String authorization = Base64.getEncoder()
.encodeToString((logonData.getIauData().getClientId() + ":" + logonData.getIauData().getClientSecret())
.getBytes(UTF_8));
URL url = new URL(httpsUrl);
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
con.setInstanceFollowRedirects(true);
// add reuqest header
con.setRequestMethod("POST");
con.setRequestProperty(CONTENT_TYPE, "application/x-www-form-urlencoded");
con.setRequestProperty(AUTHORIZATION, "Basic " + authorization);
con.setDoOutput(true);
log.info("Get token: sending 'POST' request to URL: {} with parameters: {}", url, urlParameters);
try (DataOutputStream wr = new DataOutputStream(con.getOutputStream())) {
wr.writeBytes(urlParameters);
wr.flush();
}
int responseCode = con.getResponseCode();
if (responseCode == 200) {
StringBuilder response = new StringBuilder();
try (BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
}
log.info("Get token: Response: {}", response);
JSONObject json = new JSONObject(response.toString());
return json;
} else {
String msg = con.getResponseMessage();
log.warn("Get token: Error response code: {}", responseCode, msg);
throw new Exception("Error " + responseCode + ", " + msg + " on url " + httpsUrl);
}
}
private JSONObject getUserinfo(Id4meSessionData logonData) throws Exception {
/*
* String name = "_443._tcp." + logonData.getIag() + "."; LookupResponse
* response = resolver.lookupDane(name); if (response != null) {
* log.info("TLSA lookup response: {} = \"{}\", DNSSEC = {}", name,
* response.getData(), response.isDnssec()); } else {
* log.info("TLSA lookup failed"); }
*/
String httpsUrl = logonData.getIauData().getWellKnown().getString("userinfo_endpoint");
log.info("Userinfo endpoint: " + httpsUrl);
// TODO test
String authorization = logonData.getAccessToken();
String ret = fetchUserinfo(httpsUrl, authorization);
JSONObject userinfo;
if (ret.trim().indexOf('{') == 0) {
// JSON String
userinfo = new JSONObject(ret);
} else {
userinfo = getPayloadFromJwt(ret.trim());
}
if (userinfo.has("_claim_sources") && userinfo.has("_claim_names")) {
// distributed claims
userinfo = getDistributedClaims(userinfo);
}
if (userinfo.has("error")) {
if (userinfo.has("error_description"))
throw new Exception(userinfo.getString("error_description"));
else
throw new Exception(userinfo.toString(2));
}
return userinfo;
}
private JSONObject fetchAgentJwts(String host) throws Exception {
String wellKnownUri = "https://" + host + "/.well-known/openid-configuration";
String wellKnownData = fetchUrl(wellKnownUri);
JSONObject wellKnown = new JSONObject(wellKnownData);
String jwtUri = wellKnown.getString("jwks_uri");
String jwts = fetchUrl(jwtUri);
return new JSONObject(jwts);
}
private JSONObject getDistributedClaims(JSONObject json) throws Exception {
JSONObject userinfo = new JSONObject();
JSONObject claimSources = json.getJSONObject("_claim_sources");
String[] sources = JSONObject.getNames(claimSources);
Hashtable agentJwts = new Hashtable<>();
// fetch jwts.json for any claim source
for (String src : sources) {
JSONObject ep = claimSources.getJSONObject(src);
String endpoint = ep.getString("endpoint");
URL url = new URL(endpoint);
String host = url.getHost();
// TODO check code
if (!agentJwts.containsKey(host)) {
JSONObject jwts_data = fetchAgentJwts(host);
agentJwts.put(host, jwts_data);
log.debug("+" + host);
log.debug("+" + jwts_data.toString(2));
} else {
log.debug(host);
}
}
for (String src : sources) {
JSONObject ep = claimSources.getJSONObject(src);
String endpoint = ep.getString("endpoint");
String accessToken = ep.getString("access_token");
URL url = new URL(endpoint);
String host = url.getHost();
log.debug("Get distributed claims: accessToken: {}", accessToken);
log.debug("Get distributed claims: endpoint: {}", endpoint);
log.debug("Get distributed claims: host: {}", host);
String response = fetchUserinfo(endpoint, accessToken);
SignedJWT signedToken = (SignedJWT) JWTParser.parse(response);
JSONObject jwtsData = agentJwts.get(host);
log.debug(signedToken.toString());
validateSignedToken(jwtsData, signedToken);
log.debug("Get distributed claims: response: {}", response);
JSONObject payload;
if (response.startsWith("{")) {
payload = new JSONObject(response);
} else {
payload = getPayloadFromJwt(response);
}
String[] names = JSONObject.getNames(payload);
for (String n : names) {
userinfo.put(n, payload.get(n));
}
}
return userinfo;
}
/**
* Returns the payload from a JWT as JSONObject
*
* @param token the JWT containing the payload
* @throws TokenValidationException on any validation error
* @return the JWT payload as JSONObject
*/
public JSONObject getPayloadFromJwt(String token) throws TokenValidationException {
JSONObject userinfo = null;
try {
if (token.indexOf('.') < 0) {
throw new TokenValidationException("No payload in token found.");
}
String[] idFields = token.split("\\.");
byte[] buff = Base64.getDecoder().decode(idFields[0]);
String header = new String(buff);
buff = Base64.getDecoder().decode(idFields[1]);
String payload = new String(buff);
if (idFields.length == 3) {
// validate jwts signature
}
log.info("Userinfo extracted from token: header: {}", header);
log.info("Userinfo extracted from token: payload: {}", payload);
userinfo = new JSONObject(payload);
} catch (Exception ex) {
throw new TokenValidationException(ex.getMessage());
}
return userinfo;
}
private String fetchUserinfo(String httpsUrl, String accessToken) throws Exception {
URL url = new URL(httpsUrl);
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
con.setInstanceFollowRedirects(true);
con.setRequestMethod("GET");
String authHeader = buildAuthHeader(accessToken);
con.setRequestProperty(AUTHORIZATION, authHeader);
log.info("Fetch userinfo: URL: {}", url);
log.info("Fetch userinfo: authHeader: {}", authHeader);
int status_code = con.getResponseCode();
if (status_code != 200) {
String status_message = con.getResponseMessage();
throw new Exception("Error " + status_code + ": " + status_message + ", " + httpsUrl);
}
InputStream in = con.getInputStream();
StringBuilder response = new StringBuilder();
try (InputStreamReader reader = new InputStreamReader(in); BufferedReader br = new BufferedReader(reader)) {
String input;
while ((input = br.readLine()) != null) {
response.append(input);
}
}
log.info("Fetch userinfo: response: {}", response);
return response.toString().trim();
}
private String buildAuthHeader(String authorization) {
return "Bearer " + authorization;
}
private void validateSignedToken(JSONObject jwtsData, SignedJWT signedToken) throws Exception {
log.debug("Validate signed token: {}", signedToken.toString());
JSONObject headerJson = new JSONObject(signedToken.getHeader().toString());
JSONObject payloadJson = new JSONObject(signedToken.getPayload().toString());
log.debug("Validate token: header: {}", headerJson.toString());
log.debug("Validate token: payload: {}", payloadJson.toString());
String headerKid = null;
if (headerJson.has("kid")) {
headerKid = headerJson.getString("kid");
}
if (!headerJson.has("alg")) {
throw new Exception("Field alg missing in token payload!");
}
String headerAlg = headerJson.getString("alg");
if (!headerAlg.equalsIgnoreCase("RS256")) {
throw new Exception("JWTS signature algorithm mismatch, expected RS256, found " + headerAlg);
}
validateTokenSignature(jwtsData, signedToken, headerKid, headerAlg);
}
private void validateTokenSignature(JSONObject jwtsData, SignedJWT signedToken, String headerKid, String headerAlg)
throws Exception {
JSONArray keys = jwtsData.getJSONArray("keys");
if (keys == null || keys.length() <= 0) {
throw new IllegalArgumentException("Error on validating the token, keys == NULL");
}
for (int i = 0; i < keys.length(); i++) {
JSONObject key = keys.getJSONObject(i);
String kid = key.getString("kid");
log.debug("Validating token signature: kid: {}", kid);
if (headerKid == null || kid.equals(headerKid)) {
switch (headerAlg.toUpperCase()) {
case "RS256":
RSAKey rsa = RSAKey.parse(key.toString());
RSAPublicKey pubKey = (RSAPublicKey) rsa.toPublicKey();
JWSVerifier verifier = new RSASSAVerifier(pubKey);
if (signedToken.verify(verifier)) {
log.debug("Validating token signature: token RS256 signature valid");
return;
} else {
throw new Exception("Error on validating the token signature, kid=RS256, alg=RSA");
}
default:
throw new IllegalArgumentException("Unhandled value for header_alg: " + headerAlg);
}
}
}
throw new TokenValidationException("No valid public key for token validation found!");
}
private void validateStandardClaims(Id4meSessionData sessionData, JSONObject userinfo) throws Exception {
log.debug("Validate userinfo standard claims: ", userinfo.toString());
if (!userinfo.has("nonce")) {
throw new Exception("Field nonce missing!");
} else {
String tokenNonce = userinfo.getString("nonce");
String sessionNonce = sessionData.getNonce();
if (!sessionNonce.equals(tokenNonce)) {
throw new Exception("Error on validating standard claims, the session nonce != nonce!");
}
}
if (!userinfo.has("exp")) {
throw new Exception("Field exp missing!");
}
if (!userinfo.has("iss")) {
throw new Exception("Field iss missing!");
} else {
String tokenIss = userinfo.getString("iss");
String sessionIss = sessionData.getIauData().getWellKnown().getString("issuer");
if (!sessionIss.equals(tokenIss)) {
throw new Exception("Error on validating standard claims, the session iss != iss!");
}
}
if (!userinfo.has("sub")) {
throw new Exception("Field sub missing!");
}
if (!userinfo.has("aud")) {
throw new Exception("Field aud missing!");
} else {
Object aud = userinfo.get("aud");
if (aud instanceof String) {
String sessionAud = sessionData.getIauData().getRegistrationData().getString("client_id");
if (!sessionAud.equals(aud)) {
throw new Exception("Error on validating standard claims, the session audience != aud!");
}
}
}
if (!userinfo.has("iat")) {
throw new Exception("Field iat missing!");
}
}
private void validateIdTokenPayload(Id4meSessionData sessionData, SignedJWT signedToken) throws Exception {
JSONObject payloadJson = new JSONObject(signedToken.getPayload().toString());
log.debug("Validate Id token payload: ", payloadJson.toString());
validateStandardClaims(sessionData, payloadJson);
if (payloadJson.has("exp")) {
long exp = payloadJson.getLong("exp");
if (exp != 0) {
long tokenExpiresMillis = exp * 1000;
log.debug("Validate Id token: token expiration threshold: {}", new Date(tokenExpiresMillis));
if (System.currentTimeMillis() > tokenExpiresMillis) {
throw new Exception("Token is expired!");
}
if (sessionData.getTokenExpires() != 0)
sessionData.setTokenExpires(tokenExpiresMillis);
}
}
sessionData.setStandardClaimsValidated(true);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy