001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014-2016 ForgeRock AS. 015 * Portions Copyrighted 2015 Nomura Research Institute, Ltd. 016 */ 017 018package org.forgerock.oauth2.core; 019 020import org.forgerock.guava.common.annotations.VisibleForTesting; 021import org.forgerock.json.jose.common.JwtReconstruction; 022import org.forgerock.json.jose.jws.SignedJwt; 023import org.forgerock.json.jose.jws.handlers.SigningHandler; 024import org.forgerock.util.time.TimeService; 025 026import java.util.concurrent.TimeUnit; 027 028/** 029 * Parses a JWT string and offers methods to validate the JWT is valid for the use as an OAuth2 authorization grant or 030 * for OAuth2 client authentication. 031 * 032 * @since 12.0.0 033 * @supported.all.api 034 */ 035public class OAuth2Jwt { 036 037 private static final JwtReconstruction JWT_PARSER = new JwtReconstruction(); 038 private static final long SKEW_ALLOWANCE = TimeUnit.MINUTES.toMillis(5); 039 private static final long UNREASONABLE_LIFETIME_LIMIT = TimeUnit.DAYS.toMillis(1); //TODO check this 040 041 /** 042 * Creates an {@code OAuth2Jwt} instance from the provided JWT string. 043 * 044 * @param jwtString The JWT string. 045 * @return An {@code OAuth2Jwt} instance. 046 */ 047 public static OAuth2Jwt create(String jwtString) { 048 return new OAuth2Jwt(JWT_PARSER.reconstructJwt(jwtString, SignedJwt.class), TimeService.SYSTEM); 049 } 050 051 private final SignedJwt jwt; 052 private final TimeService timeService; 053 private Boolean isSignatureValid; 054 055 @VisibleForTesting 056 OAuth2Jwt(SignedJwt jwt, TimeService timeService) { 057 this.jwt = jwt; 058 this.timeService = timeService; 059 } 060 061 /** 062 * Verifies that the JWT is valid by: 063 * <ul> 064 * <li>verifying the signature</li> 065 * <li>ensuring the JWT contains the 'iss', 'sub', 'aud' and 'exp' claims</li> 066 * <li>ensuring the JWT expiry is not unreasonably far in the future</li> 067 * <li>ensuring the JWT has not expired</li> 068 * <li>ensuring the JWT is not being used before its 'not before time'</li> 069 * <li>ensuring the JWT issued at time is not unreasonably far in the past</li> 070 * </ul> 071 * 072 * @param signingHandler The {@link SigningHandler} instance to verify the JWT signature with. 073 * @return {@code true} if the JWT meets all the expectations. 074 */ 075 public boolean isValid(SigningHandler signingHandler) { 076 if (isSignatureValid == null) { 077 isSignatureValid = jwt.verify(signingHandler); 078 } 079 return isSignatureValid && isContentValid(); 080 } 081 082 /** 083 * Verifies that the JWT is valid by: 084 * <ul> 085 * <li>ensuring the JWT contains the 'iss', 'sub', 'aud' and 'exp' claims</li> 086 * <li>ensuring the JWT expiry is not unreasonably far in the future</li> 087 * <li>ensuring the JWT has not expired</li> 088 * <li>ensuring the JWT is not being used before its 'not before time'</li> 089 * <li>ensuring the JWT issued at time is not unreasonably far in the past</li> 090 * </ul> 091 * 092 * @return {@code true} if the JWT meets all the expectations. 093 */ 094 public boolean isContentValid() { 095 096 return contains("iss", "sub", "aud", "exp") && 097 !isExpiryUnreasonable() && 098 !isExpired() && 099 !isNowBeforeNbf() && 100 !isIssuedAtUnreasonable(); 101 } 102 103 private boolean contains(String... keys) { 104 for (String key : keys) { 105 if (jwt.getClaimsSet().getClaim(key) == null) { 106 return false; 107 } 108 } 109 return true; 110 } 111 112 private boolean isExpiryUnreasonable() { 113 return jwt.getClaimsSet().getExpirationTime().getTime() > (timeService.now() + UNREASONABLE_LIFETIME_LIMIT); 114 } 115 116 /** 117 * Checks that the JWT has not expired. 118 * 119 * @return {@code true} if the JWT has expired. 120 */ 121 public boolean isExpired() { 122 return jwt.getClaimsSet().getExpirationTime().getTime() <= (timeService.now() - SKEW_ALLOWANCE); 123 } 124 125 private boolean isNowBeforeNbf() { 126 boolean present = jwt.getClaimsSet().get("nbf").getObject() != null; 127 return present && timeService.now() + SKEW_ALLOWANCE < jwt.getClaimsSet().getNotBeforeTime().getTime(); 128 } 129 130 private boolean isIssuedAtUnreasonable() { 131 boolean present = jwt.getClaimsSet().get("iat").getObject() != null; 132 return present && jwt.getClaimsSet().getIssuedAtTime().getTime() < (timeService.now() - UNREASONABLE_LIFETIME_LIMIT); 133 } 134 135 /** 136 * Checks that the JWT is intended for the provided audience. 137 * 138 * @param audience The audience. 139 * @return {@code true} if the JWT 'audience' claim contains the provided audience. 140 */ 141 public boolean isIntendedForAudience(String audience) { 142 return jwt.getClaimsSet().getAudience().contains(audience); 143 } 144 145 /** 146 * Gets the JWT subject. 147 * 148 * @return The JWT subject. 149 */ 150 public String getSubject() { 151 return jwt.getClaimsSet().getSubject(); 152 } 153 154 /** 155 * Gets the Signed JWT. 156 */ 157 public SignedJwt getSignedJwt() { 158 return jwt; 159 } 160}