001package com.nimbusds.oauth2.sdk.assertions.saml2;
002
003
004import java.util.Date;
005import java.util.Set;
006
007import com.nimbusds.jwt.proc.ClockSkewAware;
008import com.nimbusds.jwt.util.DateUtils;
009import com.nimbusds.oauth2.sdk.id.Audience;
010import net.jcip.annotations.Immutable;
011import org.apache.commons.collections4.CollectionUtils;
012
013
014/**
015 * SAML 2.0 bearer assertion details verifier for OAuth 2.0 client
016 * authentication and authorisation grants. Intended for initial validation of
017 * SAML 2.0 assertions:
018 *
019 * <ul>
020 *     <li>Audience check
021 *     <li>Expiration time check
022 *     <li>Not-before time check (is set)
023 * </ul>
024 *
025 * <p>Related specifications:
026 *
027 * <ul>
028 *     <li>Security Assertion Markup Language (SAML) 2.0 Profile for OAuth 2.0
029 *         Client Authentication and Authorization Grants (RFC 7522).
030 * </ul>
031 */
032@Immutable
033public class SAML2AssertionDetailsVerifier implements ClockSkewAware {
034
035
036        /**
037         * The default maximum acceptable clock skew, in seconds (60).
038         */
039        public static final int DEFAULT_MAX_CLOCK_SKEW_SECONDS = 60;
040        
041
042        // Cache SAML exceptions to speed up processing
043
044
045        /**
046         * Expired SAML 2.0 assertion exception.
047         */
048        private static final BadSAML2AssertionException EXPIRED_SAML2_ASSERTION_EXCEPTION =
049                new BadSAML2AssertionException("Expired SAML 2.0 assertion");
050
051
052        /**
053         * SAML 2.0 assertion before use time.
054         */
055        private static final BadSAML2AssertionException SAML2_ASSERTION_BEFORE_USE_EXCEPTION =
056                new BadSAML2AssertionException("SAML 2.0 assertion before use time");
057
058
059        /**
060         * The expected audience.
061         */
062        private final Set<Audience> expectedAudience;
063
064
065        /**
066         * Cached unexpected SAML 2.0 audience exception.
067         */
068        private final BadSAML2AssertionException unexpectedAudienceException;
069
070
071        /**
072         * The maximum acceptable clock skew, in seconds.
073         */
074        private int maxClockSkewSeconds = DEFAULT_MAX_CLOCK_SKEW_SECONDS;
075
076
077        /**
078         * Creates a new SAML 2.0 bearer assertion details verifier.
079         *
080         * @param expectedAudience The expected audience values. Must not be
081         *                         empty or {@code null}. Should typically
082         *                         contain the token endpoint URI and for
083         *                         OpenID provider it may also include the
084         *                         issuer URI.
085         */
086        public SAML2AssertionDetailsVerifier(final Set<Audience> expectedAudience) {
087                if (CollectionUtils.isEmpty(expectedAudience)) {
088                        throw new IllegalArgumentException("The expected audience set must not be null or empty");
089                }
090
091                this.expectedAudience = expectedAudience;
092
093                unexpectedAudienceException = new BadSAML2AssertionException("Invalid SAML 2.0 audience, expected " + expectedAudience);
094        }
095
096
097        /**
098         * Returns the expected audience values.
099         *
100         * @return The expected audience values.
101         */
102        public Set<Audience> getExpectedAudience() {
103                return expectedAudience;
104        }
105
106
107        @Override
108        public int getMaxClockSkew() {
109                return maxClockSkewSeconds;
110        }
111
112
113        @Override
114        public void setMaxClockSkew(int maxClockSkewSeconds) {
115                this.maxClockSkewSeconds = maxClockSkewSeconds;
116        }
117
118
119        /**
120         * Verifies the specified SAML 2.0 bearer assertion details.
121         *
122         * @param assertionDetails The SAML 2.0 bearer assertion details. Must
123         *                         not be {@code null}.
124         *
125         * @throws BadSAML2AssertionException If verification didn't pass
126         *                                    successfully.
127         */
128        public void verify(final SAML2AssertionDetails assertionDetails)
129                throws BadSAML2AssertionException {
130
131                // Check audience
132                if (! Audience.matchesAny(expectedAudience, assertionDetails.getAudience())) {
133                        throw unexpectedAudienceException;
134                }
135
136                // Check expiration
137                final Date now = new Date();
138
139                if (! DateUtils.isAfter(assertionDetails.getExpirationTime(), now, maxClockSkewSeconds)) {
140                        throw EXPIRED_SAML2_ASSERTION_EXCEPTION;
141                }
142
143                // Check optional not before use time
144                if (assertionDetails.getNotBeforeTime() != null) {
145                        if (! DateUtils.isBefore(assertionDetails.getNotBeforeTime(), now, maxClockSkewSeconds))
146                                throw SAML2_ASSERTION_BEFORE_USE_EXCEPTION;
147                }
148        }
149}