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