All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.id4me.Id4meLogon Maven / Gradle / Ivy

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