001package com.nimbusds.oauth2.sdk.assertions.jwt; 002 003 004import java.util.Set; 005 006import com.nimbusds.jwt.JWTClaimsSet; 007import com.nimbusds.jwt.proc.BadJWTException; 008import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier; 009import com.nimbusds.oauth2.sdk.id.Audience; 010import net.jcip.annotations.Immutable; 011import org.apache.commons.collections4.CollectionUtils; 012 013 014/** 015 * JSON Web Token (JWT) bearer assertion details (claims set) verifier for 016 * OAuth 2.0 client authentication and authorisation grants. Intended for 017 * initial validation of JWT assertions: 018 * 019 * <ul> 020 * <li>Audience check 021 * <li>Expiration time check 022 * <li>Not-before time check (is set) 023 * <li>Subject and issuer presence check 024 * </ul> 025 * 026 * <p>Related specifications: 027 * 028 * <ul> 029 * <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and 030 * Authorization Grants (RFC 7523). 031 * </ul> 032 */ 033@Immutable 034public class JWTAssertionDetailsVerifier extends DefaultJWTClaimsVerifier { 035 036 037 // Cache JWT exceptions for quick processing of bad claims sets 038 039 040 /** 041 * Missing JWT expiration claim. 042 */ 043 private static final BadJWTException MISSING_EXP_CLAIM_EXCEPTION = 044 new BadJWTException("Missing JWT expiration claim"); 045 046 047 /** 048 * Missing JWT audience claim. 049 */ 050 private static final BadJWTException MISSING_AUD_CLAIM_EXCEPTION = 051 new BadJWTException("Missing JWT audience claim"); 052 053 054 /** 055 * Missing JWT subject claim. 056 */ 057 private static final BadJWTException MISSING_SUB_CLAIM_EXCEPTION = 058 new BadJWTException("Missing JWT subject claim"); 059 060 061 /** 062 * Missing JWT issuer claim. 063 */ 064 private static final BadJWTException MISSING_ISS_CLAIM_EXCEPTION = 065 new BadJWTException("Missing JWT issuer claim"); 066 067 068 /** 069 * The expected audience. 070 */ 071 private final Set<Audience> expectedAudience; 072 073 074 /** 075 * Cached unexpected JWT audience claim exception. 076 */ 077 private final BadJWTException unexpectedAudClaimException; 078 079 080 /** 081 * Creates a new JWT bearer assertion details (claims set) verifier. 082 * 083 * @param expectedAudience The expected audience (aud) claim values. 084 * Must not be empty or {@code null}. Should 085 * typically contain the token endpoint URI and 086 * for OpenID provider it may also include the 087 * issuer URI. 088 */ 089 public JWTAssertionDetailsVerifier(final Set<Audience> expectedAudience) { 090 091 if (CollectionUtils.isEmpty(expectedAudience)) { 092 throw new IllegalArgumentException("The expected audience set must not be null or empty"); 093 } 094 095 this.expectedAudience = expectedAudience; 096 097 unexpectedAudClaimException = new BadJWTException("Invalid JWT audience claim, expected " + expectedAudience); 098 } 099 100 101 /** 102 * Returns the expected audience values. 103 * 104 * @return The expected audience (aud) claim values. 105 */ 106 public Set<Audience> getExpectedAudience() { 107 108 return expectedAudience; 109 } 110 111 112 @Override 113 public void verify(final JWTClaimsSet claimsSet) 114 throws BadJWTException { 115 116 super.verify(claimsSet); 117 118 if (claimsSet.getExpirationTime() == null) { 119 throw MISSING_EXP_CLAIM_EXCEPTION; 120 } 121 122 if (claimsSet.getAudience() == null || claimsSet.getAudience().isEmpty()) { 123 throw MISSING_AUD_CLAIM_EXCEPTION; 124 } 125 126 boolean audMatch = false; 127 128 for (String aud: claimsSet.getAudience()) { 129 130 if (aud == null || aud.isEmpty()) { 131 continue; // skip 132 } 133 134 if (expectedAudience.contains(new Audience(aud))) { 135 audMatch = true; 136 } 137 } 138 139 if (! audMatch) { 140 throw unexpectedAudClaimException; 141 } 142 143 if (claimsSet.getIssuer() == null) { 144 throw MISSING_ISS_CLAIM_EXCEPTION; 145 } 146 147 if (claimsSet.getSubject() == null) { 148 throw MISSING_SUB_CLAIM_EXCEPTION; 149 } 150 } 151}