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}