001package com.nimbusds.openid.connect.provider.spi.tokens;
002
003
004import java.util.*;
005
006import net.jcip.annotations.ThreadSafe;
007import net.minidev.json.JSONObject;
008
009import com.nimbusds.jose.JOSEObjectType;
010import com.nimbusds.jwt.JWTClaimsSet;
011import com.nimbusds.oauth2.sdk.auth.X509CertificateConfirmation;
012import com.nimbusds.oauth2.sdk.dpop.JWKThumbprintConfirmation;
013import com.nimbusds.oauth2.sdk.id.*;
014
015
016/**
017 * Base implementation of the SPI for encoding and decoding authorisations for
018 * self-contained access tokens into JWT claims sets.
019 *
020 * <p>Provides encoding and decoding for all token parameters for which there
021 * is an appropriate standard JWT claim (see JSON Web Token (JWT) (RFC 7519),
022 * section 4.1, OAuth 2.0 Mutual-TLS Client Authentication and
023 * Certificate-Bound Access Tokens (RFC 8705), section 3.1), and OAuth 2.0
024 * Demonstrating Proof-of-Possession at the Application Layer (DPoP)
025 * (draft-ietf-oauth-dpop-16), section 6:
026 *
027 * <ul>
028 *     <li>{@link AccessTokenAuthorization#getSubject subject} - "sub"
029 *     <li>{@link AccessTokenAuthorization#getActor actor} - "act"
030 *     <li>{@link AccessTokenAuthorization#getExpirationTime expiration time} - "exp"
031 *     <li>{@link AccessTokenAuthorization#getIssueTime issue time} - "iat"
032 *     <li>{@link AccessTokenAuthorization#getIssuer issuer} - "iss"
033 *     <li>{@link AccessTokenAuthorization#getAudienceList audience} - "aud"
034 *     <li>{@link AccessTokenAuthorization#getJWTID JWT ID} - "jti"
035 *     <li>{@link AccessTokenAuthorization#getClientCertificateConfirmation client X.509 certificate SHA-256 thumbprint (mTLS)} - "cnf.x5t#S256"
036 *     <li>{@link AccessTokenAuthorization#getJWKThumbprintConfirmation  JWK SHA-256 thumbprint confirmation (DPoP)} - "cnf.jkt"
037 * </ul>
038 *
039 * <p>The extending class should implement encoding and decoding for the
040 * remaining token parameters:
041 *
042 * <ul>
043 *     <li>{@link AccessTokenAuthorization#getClientID client ID}
044 *     <li>{@link AccessTokenAuthorization#getScope scope}
045 *     <li>{@link AccessTokenAuthorization#getClaimNames consented OpenID claim names}
046 *     <li>{@link AccessTokenAuthorization#getClaimsLocales preferred claims locales}
047 *     <li>{@link AccessTokenAuthorization#getPresetClaims preset OpenID claims}
048 *     <li>{@link AccessTokenAuthorization#getClaimsData claims fulfillment data}
049 *     <li>{@link AccessTokenAuthorization#getData additional data}
050 *     <li>{@link AccessTokenAuthorization#getOtherTopLevelParameters other top-level parameters}
051 * </ul>
052 */
053@ThreadSafe
054public abstract class BaseSelfContainedAccessTokenClaimsCodec implements SelfContainedAccessTokenClaimsCodec {
055        
056        
057        /**
058         * The supported claim names.
059         */
060        public static final Set<String> SUPPORTED_CLAIM_NAMES = Collections.unmodifiableSet(
061                new HashSet<>(Arrays.asList(
062                        "sub", "act", "exp", "iat", "iss", "aud", "jti", "cnf"
063                ))
064        );
065        
066        
067        @Override
068        public JWTClaimsSet encode(final AccessTokenAuthorization tokenAuthz, final TokenEncoderContext context) {
069                
070                JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
071                
072                if (tokenAuthz.getSubject()        != null) builder.subject(tokenAuthz.getSubject().getValue());
073                if (tokenAuthz.getActor()          != null) builder.claim("act", tokenAuthz.getActor().toJSONObject());
074                if (tokenAuthz.getExpirationTime() != null) builder.expirationTime(Date.from(tokenAuthz.getExpirationTime()));
075                if (tokenAuthz.getIssueTime()      != null) builder.issueTime(Date.from(tokenAuthz.getIssueTime()));
076                if (tokenAuthz.getIssuer()         != null) builder.issuer(tokenAuthz.getIssuer().getValue());
077                if (tokenAuthz.getAudienceList()   != null) builder.audience(Audience.toStringList(tokenAuthz.getAudienceList()));
078                if (tokenAuthz.getJWTID()          != null) builder.jwtID(tokenAuthz.getJWTID().getValue());
079                
080                JSONObject cnfValue = new JSONObject();
081                if (tokenAuthz.getClientCertificateConfirmation() != null) {
082                        cnfValue.putAll(tokenAuthz.getClientCertificateConfirmation().toJWTClaim().getValue());
083                }
084                if (tokenAuthz.getJWKThumbprintConfirmation() != null) {
085                        cnfValue.putAll(tokenAuthz.getJWKThumbprintConfirmation().toJWTClaim().getValue());
086                }
087                if (! cnfValue.isEmpty()) {
088                        builder.claim("cnf", cnfValue);
089                }
090                
091                return builder.build();
092        }
093        
094        
095        @Override
096        public JWTDetails advancedEncode(final AccessTokenAuthorization tokenAuthz, final TokenEncoderContext context) {
097                return new JWTDetails() {
098                        @Override
099                        public JOSEObjectType getType() {
100                                return null;
101                        }
102                        
103                        
104                        @Override
105                        public JWTClaimsSet getJWTClaimsSet() {
106                                return encode(tokenAuthz, context);
107                        }
108                };
109        }
110        
111        
112        @Override
113        public AccessTokenAuthorization decode(final JWTClaimsSet claimsSet, final TokenCodecContext context)
114                throws TokenDecodeException {
115                
116                MutableAccessTokenAuthorization authz = new MutableAccessTokenAuthorization();
117                
118                String sub = claimsSet.getSubject();
119                if (sub != null) authz.withSubject(new Subject(sub));
120                
121                try {
122                        Map<String, Object> act = claimsSet.getJSONObjectClaim("act");
123                        if (act != null) authz.withActor(Actor.parse(new JSONObject(act)));
124                } catch (Exception e) {
125                        throw new TokenDecodeException("Couldn't parse actor: " + e.getMessage(), e);
126                }
127                
128                Date exp = claimsSet.getExpirationTime();
129                if (exp != null) authz.withExpirationTime(exp.toInstant());
130                
131                Date iat = claimsSet.getIssueTime();
132                if (iat != null) authz.withIssueTime(iat.toInstant());
133                
134                String iss = claimsSet.getIssuer();
135                if (iss != null) authz.withIssuer(new Issuer(iss));
136                
137                List<String> aud = claimsSet.getAudience();
138                if (aud != null && ! aud.isEmpty()) authz.withAudienceList(Audience.create(aud));
139                
140                String jti = claimsSet.getJWTID();
141                if (jti != null) authz.withJWTID(new JWTID(jti));
142                
143                authz.withClientCertificateConfirmation(X509CertificateConfirmation.parse(claimsSet));
144                
145                authz.withJWKThumbprintConfirmation(JWKThumbprintConfirmation.parse(claimsSet));
146                
147                return authz;
148        }
149        
150        
151        @Override
152        public AccessTokenAuthorization advancedDecode(final JWTDetails jwtDetails, final TokenCodecContext context)
153                throws TokenDecodeException {
154                
155                return decode(jwtDetails.getJWTClaimsSet(), context);
156        }
157}