SelfIssuedAccessToken.java

package iu.auth.config;

import java.net.URI;
import java.net.http.HttpRequest.Builder;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import java.util.Set;

import javax.security.auth.Subject;

import edu.iu.IuObject;
import edu.iu.auth.IuApiCredentials;
import edu.iu.auth.IuAuthenticationException;
import edu.iu.auth.config.IuPrivateKeyPrincipal;
import edu.iu.auth.oauth.IuCallerAttributes;
import iu.crypt.Jwt;

/**
 * Encapsulates a self-issued access token suitable for server-to-server
 * invocation between nodes associated by a web of trust relationship.
 */
public class SelfIssuedAccessToken implements IuApiCredentials {

	private final String bearerToken;
	private final RemoteAccessToken accessToken;

	/**
	 * Constructor, for use by the self-issuing client endpoint.
	 * 
	 * @param pkp      Issuer identity metadata
	 * @param audience Remote audience URI
	 * @param tokenTtl Duration between token issue and expiration times
	 * @param caller   Caller attributes
	 */
	public SelfIssuedAccessToken(IuPrivateKeyPrincipal pkp, URI audience, Duration tokenTtl,
			IuCallerAttributes caller) {

		Objects.requireNonNull(caller, "missing caller attributes");
		final var authnPrincipal = Objects.requireNonNull(caller.getAuthnPrincipal(), "missing authn_principal");
		Objects.requireNonNull(caller.getRemoteAddr(), "missing remote_addr");
		Objects.requireNonNull(caller.getRequestUri(), "missing request_uri");
		Objects.requireNonNull(caller.getUserAgent(), "missing user_agent");

		final var impersonatedPrincipal = caller.getImpersonatedPrincipal();
		final var sub = impersonatedPrincipal == null ? authnPrincipal : impersonatedPrincipal;

		final var jwk = pkp.getJwk();
		final var iss = URI.create(jwk.getKeyId());
		accessToken = RemoteAccessToken.builder().jti() //
				.iss(iss).aud(audience).sub(sub) //
				.iat().exp(Instant.now().plus(tokenTtl)) //
				.caller(caller).build();
		bearerToken = accessToken.sign("JWT", pkp.getAlg(), jwk);
	}

	/**
	 * Constructor, for use by the verifying server endpoint.
	 * 
	 * @param pkp         Issuer identity metadata
	 * @param audience    Remote audience URI
	 * @param tokenTtl    Duration between token issue and expiration times
	 * @param bearerToken Bearer token
	 */
	public SelfIssuedAccessToken(IuPrivateKeyPrincipal pkp, URI audience, Duration tokenTtl, String bearerToken) {
		this.bearerToken = bearerToken;
		final var jwk = pkp.getJwk();
		final var iss = URI.create(jwk.getKeyId());
		accessToken = new RemoteAccessToken(Jwt.verify(bearerToken, jwk));
		accessToken.validateClaims(audience, tokenTtl);
		IuObject.once(iss, accessToken.getIssuer(), "issuer mismatch");

		final var caller = Objects.requireNonNull(accessToken.getCallerAttributes(), "missing caller attributes");
		final var authnPrincipal = Objects.requireNonNull(caller.getAuthnPrincipal(), "missing authn_principal");
		Objects.requireNonNull(caller.getRemoteAddr(), "missing remote_addr");
		Objects.requireNonNull(caller.getRequestUri(), "missing request_uri");
		Objects.requireNonNull(caller.getUserAgent(), "missing user_agent");

		final var sub = accessToken.getSubject();
		if (!authnPrincipal.equals(sub) //
				&& !sub.equals(caller.getImpersonatedPrincipal()))
			throw new IllegalArgumentException("sub mismatch");
	}

	@Override
	public String getName() {
		return accessToken.getSubject();
	}

	@Override
	public String getIssuer() {
		return accessToken.getIssuer().toString();
	}

	@Override
	public Instant getIssuedAt() {
		return accessToken.getIssuedAt();
	}

	@Override
	public Instant getAuthTime() {
		return accessToken.getIssuedAt();
	}

	@Override
	public Instant getExpires() {
		return accessToken.getExpires();
	}

	@Override
	public Subject getSubject() {
		return new Subject(true, Set.of(this), Set.of(accessToken.getCallerAttributes()), Set.of());
	}

	@Override
	public void applyTo(Builder requestBuilder) throws IuAuthenticationException {
		requestBuilder.header("Authorization", //
				"Bearer " + bearerToken);
	}

	@Override
	public String toString() {
		return "SelfIssuedAccessToken [" + accessToken + "]";
	}

}