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

com.sap.cloud.security.xsuaa.test.JwtGenerator Maven / Gradle / Ivy

There is a newer version: 3.5.6
Show newest version
package com.sap.cloud.security.xsuaa.test;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.*;

import org.apache.commons.io.IOUtils;
import org.springframework.lang.Nullable;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.oauth2.jwt.Jwt;

import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTParser;
import org.springframework.util.Assert;

import static com.sap.cloud.security.xsuaa.test.JwtGenerator.TokenHeaders.JKU;
import static com.sap.cloud.security.xsuaa.test.JwtGenerator.TokenHeaders.KID;

/**
 * Create tokens with a fixed private/public key and dummy values. The client
 * ID, identity zone, and scopes are configurable.
 */
public class JwtGenerator {

	/**
	 * Do not want to introduce circular reference to spring-xsuaa. duplicate of
	 * com.sap.cloud.security.xsuaa.token.TokenClaims
	 */
	public final class TokenClaims {
		private TokenClaims() {
			throw new IllegalStateException("Utility class");
		}

		static final String CLAIM_XS_USER_ATTRIBUTES = "xs.user.attributes";
		static final String CLAIM_SCOPES = "scope";
		static final String CLAIM_CLIENT_ID = "cid";
		static final String CLAIM_USER_NAME = "user_name";
		static final String CLAIM_EMAIL = "email";
		static final String CLAIM_ORIGIN = "origin";
		static final String CLAIM_GRANT_TYPE = "grant_type";
		static final String CLAIM_ZDN = "zdn";
		static final String CLAIM_ZONE_ID = "zid";
		static final String CLAIM_EXTERNAL_ATTR = "ext_attr";
	}

	public final class TokenHeaders {
		private TokenHeaders() {
			throw new IllegalStateException("Utility class");
		}

		static final String JKU = "jku";
		static final String KID = "kid";
	}

	// must match the port defined in XsuaaMockWebServer
	private static final int MOCK_XSUAA_DEFAULT_PORT = 33195;
	private static final String INITIAL_JKU = "null";
	public static final Date NO_EXPIRE_DATE = new GregorianCalendar(2190, 11, 31).getTime();
	public static final int NO_EXPIRE = Integer.MAX_VALUE;
	public static final String CLIENT_ID = "sb-xsapplication!t895";
	public static final String DEFAULT_IDENTITY_ZONE_ID = "uaa";
	private static final String PRIVATE_KEY_FILE = "/privateKey.txt";
	// see XsuaaToken.GRANTTYPE_SAML2BEARER
	private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:saml2-bearer";

	private final String clientId;
	private final String identityZoneId;
	private final String subdomain;

	private String jku = INITIAL_JKU;
	private String[] scopes;
	private String userName = "testuser";
	private String jwtHeaderKeyId = "legacy-token-key";
	private int port;
	private Map> attributes = new HashMap<>();
	private Map customClaims = new LinkedHashMap();
	private boolean deriveAudiences = false;

	/**
	 * Specifies clientId of the JWT token claim.
	 *
	 * @param clientId
	 *            the XSUAA client id, e.g. sb-applicationName!t123, defines the
	 *            value of the JWT token claims "client_id" and "cid". A token is
	 *            considered to be valid when it matches the "xsuaa.clientid" xsuaa
	 *            service configuration (VCAP_SERVICES).
	 */
	public JwtGenerator(String clientId) {
		this(clientId, MOCK_XSUAA_DEFAULT_PORT);
	}

	/**
	 * Specifies clientId of the JWT token claim.
	 *
	 * @param clientId
	 *            the XSUAA client id, e.g. sb-applicationName!t123, defines the
	 *            value of the JWT token claims "client_id" and "cid". A token is
	 *            considered to be valid when it matches the "xsuaa.clientid" xsuaa
	 *            service configuration (VCAP_SERVICES).
	 * @param port
	 *            the port that is used to connect to the XSUAA mock web server.
	 */
	public JwtGenerator(String clientId, int port) {
		this(clientId, "", DEFAULT_IDENTITY_ZONE_ID);
		this.port = port;
	}

	/**
	 * Overwrites some default values of the JWT token claims.
	 *
	 * @param clientId
	 *            the XSUAA client id, e.g. sb-applicationName!t123, defines the
	 *            value of the JWT token claims "client_id" and "cid". A token is
	 *            considered to be valid when it matches the "xsuaa.clientid" xsuaa
	 *            service configuration (VCAP_SERVICES).
	 * @param subdomain
	 *            of the subaccount, e.g. d012345trial. This defines the value of
	 *            the claim "zdn". Furthermore the identity-zone-id claim "zid" is
	 *            derived from that.
	 */
	public JwtGenerator(String clientId, String subdomain) {
		this(clientId, subdomain, subdomain + "-id");
	}

	public JwtGenerator(String clientId, String subdomain, String identityZoneId) {
		this.clientId = clientId;
		this.subdomain = subdomain;
		this.identityZoneId = identityZoneId;
		this.port = MOCK_XSUAA_DEFAULT_PORT;
	}

	/**
	 *
	 * @param port
	 *            the port that is used to connect to the XSUAA mock web server.
	 */
	public JwtGenerator(int port) {
		this(CLIENT_ID, port);
	}

	public JwtGenerator() {
		this(CLIENT_ID);
	}

	/**
	 * Changes the value of the jwt claim "user_name". The user name is also used
	 * for the "email" claim.
	 *
	 * @param userName
	 *            the user name
	 * @return the JwtGenerator itself
	 */
	public JwtGenerator setUserName(String userName) {
		this.userName = userName;
		return this;
	}

	/**
	 * Sets the roles as claim "scope" to the jwt.
	 *
	 * @param scopes
	 *            the scopes that should be part of the token
	 * @return the JwtGenerator itself
	 */
	public JwtGenerator addScopes(String... scopes) {
		this.scopes = scopes;
		return this;
	}

	/**
	 * Adds the attributes as claim "xs.user.attribute" to the jwt.
	 *
	 * @param attributeName
	 *            the attribute name that should be part of the token
	 * @param attributeValues
	 *            the attribute value that should be part of the token
	 * @return the JwtGenerator itself
	 */
	public JwtGenerator addAttribute(String attributeName, String[] attributeValues) {
		List valueList = new ArrayList<>(Arrays.asList(attributeValues));
		attributes.put(attributeName, valueList);
		return this;
	}

	/**
	 * Sets the keyId value as "kid" header to the jwt.
	 *
	 * @param keyId
	 *            the value of the signed jwt token header "kid"
	 * @return the JwtGenerator itself
	 */
	public JwtGenerator setJwtHeaderKeyId(String keyId) {
		this.jwtHeaderKeyId = keyId;
		return this;
	}

	/**
	 * Adds additional custom claims.
	 *
	 * @param customClaims
	 *            the claims that should be part of the token
	 * @return the JwtGenerator itself
	 */
	public JwtGenerator addCustomClaims(Map customClaims) {
		this.customClaims.putAll(customClaims);
		return this;
	}

	/**
	 * Derives audiences claim ("aud") from scopes. For example in case e.g.
	 * "xsappid.scope".
	 *
	 * @param shallDeriveAudiences
	 *            if true, audiences are automatically set
	 * @return the JwtGenerator itself
	 */
	public JwtGenerator deriveAudiences(boolean shallDeriveAudiences) {
		this.deriveAudiences = shallDeriveAudiences;
		return this;
	}

	/**
	 * Returns an encoded JWT token for the "Authorization" REST header
	 *
	 * @return jwt token String with "Bearer " prefix
	 */
	public String getTokenForAuthorizationHeader() {
		try {
			return "Bearer " + getToken().getTokenValue();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * Sets the url which is used to retrieve the verification keys
	 *
	 * @param jku
	 *            the url to retrieve the keys
	 * @return the JwtGenerator itself
	 */
	public JwtGenerator setJku(String jku) {
		this.jku = jku;
		return this;
	}

	/**
	 * Sets the port which is used to retrieve the verification keys
	 *
	 * @param port
	 *            the port that is used to connect to the XSUAA mock web server.
	 * @return the JwtGenerator itself
	 */
	public JwtGenerator setPort(int port) {
		this.port = port;
		return this;
	}

	/**
	 * Builds a basic Jwt with the given clientId, userName, scopes, user attributes
	 * claims and the keyId header.
	 *
	 * @return jwt
	 */
	public Jwt getToken() {
		JWTClaimsSet.Builder claimsSetBuilder = getBasicClaimSet();

		if (scopes != null && scopes.length > 0) {
			claimsSetBuilder.claim(TokenClaims.CLAIM_SCOPES, scopes);
			if (deriveAudiences) {
				claimsSetBuilder.audience(deriveAudiencesFromScopes(scopes));
			}
		}
		if (attributes.size() > 0) {
			claimsSetBuilder.claim(TokenClaims.CLAIM_XS_USER_ATTRIBUTES, attributes);
		}
		for (Map.Entry customClaim : customClaims.entrySet()) {
			claimsSetBuilder.claim(customClaim.getKey(), customClaim.getValue());
		}
		return createFromClaims(claimsSetBuilder.build().toString(), getHeaderMap(jwtHeaderKeyId, getOrCreateJku()));
	}

	private String getOrCreateJku() {
		if (INITIAL_JKU.equals(jku)) {
			String subdomainPart = subdomain != null && !subdomain.equals("") ? "/" + subdomain : "";
			return "http://localhost:" + port + subdomainPart + "/token_keys";
		}
		return jku;
	}

	private List deriveAudiencesFromScopes(String[] scopes) {
		List audiences = new ArrayList<>();
		for (String scope : scopes) {
			if (scope.contains(".")) {
				String aud = scope.substring(0, scope.indexOf('.'));
				if (aud.isEmpty() || audiences.contains(aud)) {
					break;
				}
				audiences.add(aud);
			}
		}
		return audiences;
	}

	/**
	 * Creates a Jwt from a template file, which contains the claims. Optionally,
	 * configure the "keyId" header via {@link #setJwtHeaderKeyId(String)}
	 *
	 * This replaces these placeholders:
	 * 
    *
  • "$exp" with a date, that will not expire
  • *
  • "$clientid" with the configured client id * {@link #JwtGenerator(String)}
  • *
  • "$zdn" with the configured subdomain * {@link #JwtGenerator(String, String)}
  • *
  • "$zid" with "uaa" or with the configured subdomain * {@link #JwtGenerator(String,String)}
  • *
  • "$username" with the configured user name {@link #setUserName(String)} *
  • *
* * @param pathToTemplate * classpath resource * @return a jwt * @throws IOException * in case the template file can not be read */ public Jwt createFromTemplate(String pathToTemplate) throws IOException { String claimsFromTemplate = IOUtils.resourceToString(pathToTemplate, StandardCharsets.UTF_8); String claimsWithReplacements = replacePlaceholders(claimsFromTemplate); return createFromClaims(claimsWithReplacements, getHeaderMap(jwtHeaderKeyId, getOrCreateJku())); } /** * Creates a Jwt from a file, which contains an encoded Jwt token. * * @param pathToJwt * classpath resource * @return a jwt * @throws IOException * in case the template file can not be read */ public static Jwt createFromFile(String pathToJwt) throws IOException { return convertTokenToOAuthJwt(IOUtils.resourceToString(pathToJwt, Charset.forName("UTF-8"))); } /** * Creates an individual Jwt based on the provided set of claims. * * @param claimsSet * that can be created with Nimbus JOSE + JWT JWTClaimsSet.Builder * @return a jwt */ public static Jwt createFromClaims(JWTClaimsSet claimsSet) { return createFromClaims(claimsSet.toString(), Collections.EMPTY_MAP); } /** * Creates an individual Jwt based on the provided set of claims. * * @param claimsSet * that can be created with Nimbus JOSE + JWT JWTClaimsSet.Builder * @param tokenHeaders * that contains a set of headers that should be included in the * final jwt. * @return a jwt */ public static Jwt createFromClaims(JWTClaimsSet claimsSet, Map tokenHeaders) { return createFromClaims(claimsSet.toString(), tokenHeaders); } /** * Builds a basic set of claims * * @return a basic set of claims */ public JWTClaimsSet.Builder getBasicClaimSet() { return new JWTClaimsSet.Builder() .issueTime(new Date()) .expirationTime(JwtGenerator.NO_EXPIRE_DATE) .claim(TokenClaims.CLAIM_CLIENT_ID, clientId) .claim(TokenClaims.CLAIM_ORIGIN, "userIdp") .claim(TokenClaims.CLAIM_USER_NAME, userName) .claim(TokenClaims.CLAIM_EMAIL, userName + "@test.org") .claim(TokenClaims.CLAIM_ZDN, subdomain) .claim(TokenClaims.CLAIM_ZONE_ID, identityZoneId) .claim(TokenClaims.CLAIM_EXTERNAL_ATTR, new ExternalAttrClaim()) .claim(TokenClaims.CLAIM_GRANT_TYPE, GRANT_TYPE); } /** * Builds a basic set of claims * * @return a basic set of claims */ public Map getBasicHeaders() { return getHeaderMap(jwtHeaderKeyId, getOrCreateJku()); } private static Jwt createFromClaims(String claims, Map headers) { String token = signAndEncodeToken(claims, headers); return convertTokenToOAuthJwt(token); } private static Map getHeaderMap(String jwtHeaderKeyId, String jwtKeyUrl) { Map headers = new HashMap<>(); if (jwtHeaderKeyId != null) { headers.put(KID, jwtHeaderKeyId); } if (jwtKeyUrl != null) { headers.put(JKU, jwtKeyUrl); } return headers; } private String replacePlaceholders(String claims) { claims = claims.replace("$exp", String.valueOf(NO_EXPIRE)); claims = claims.replace("$clientid", clientId); claims = claims.replace("$zdn", subdomain); claims = claims.replace("$zid", identityZoneId); claims = claims.replace("$username", userName); return claims; } private static String signAndEncodeToken(String claims, Map tokenHeaders) { RsaSigner signer = new RsaSigner(readPrivateKeyFromFile()); org.springframework.security.jwt.Jwt jwt = JwtHelper.encode(claims, signer, tokenHeaders); return jwt.getEncoded(); } protected static String readPrivateKeyFromFile() { String privateKey; try { privateKey = IOUtils.resourceToString(PRIVATE_KEY_FILE, StandardCharsets.UTF_8); // PEM format } catch (IOException e) { throw new IllegalStateException("privateKey could not be read from " + PRIVATE_KEY_FILE, e); } return privateKey; } @Nullable public static Jwt convertTokenToOAuthJwt(String token) { Jwt jwt; try { JWT parsedJwt = JWTParser.parse(token); JWTClaimsSet jwtClaimsSet = parsedJwt.getJWTClaimsSet(); Map headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject()); jwt = new Jwt(parsedJwt.getParsedString(), jwtClaimsSet.getIssueTime().toInstant(), jwtClaimsSet.getExpirationTime().toInstant(), headers, jwtClaimsSet.getClaims()); } catch (ParseException e) { throw new IllegalArgumentException("token can not be parsed. ", e); } return jwt; } class ExternalAttrClaim { public String zdn = subdomain; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy