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 ForgeRock AS.
015 */
016
017package org.forgerock.openig.filter.oauth2.resolver;
018
019import static java.util.concurrent.TimeUnit.*;
020
021import java.util.Collections;
022import java.util.HashSet;
023import java.util.Map;
024import java.util.Set;
025
026import org.forgerock.json.fluent.JsonValue;
027import org.forgerock.json.fluent.JsonValueException;
028import org.forgerock.openig.filter.oauth2.AccessToken;
029import org.forgerock.openig.filter.oauth2.OAuth2TokenException;
030import org.forgerock.util.time.TimeService;
031
032/**
033 * Models an {@link AccessToken} as returned by the OpenAM {@literal tokeninfo} endpoint.
034 * <pre>
035 *     curl https://openam.example.com:8443/openam/oauth2/tokeninfo?access_token=70e5776c-b0fa-4c70-9962-defb0e9c3cd6
036 * </pre>
037 *
038 * Example of OpenAM returned Json value (for the previous request):
039 * <pre>
040 *     {
041 *         "scope": [
042 *             "email",
043 *             "profile"
044 *         ],
045 *         "grant_type": "password",
046 *         "realm": "/",
047 *         "token_type": "Bearer",
048 *         "expires_in": 471,
049 *         "access_token": "70e5776c-b0fa-4c70-9962-defb0e9c3cd6",
050 *         "email": "",
051 *         "profile": ""
052 *     }
053 * </pre>
054 */
055public class OpenAmAccessToken implements AccessToken {
056
057    private final JsonValue rawInfo;
058    private final String token;
059    private final Set<String> scopes;
060    private final long expiresAt;
061
062    /**
063     * Builds a {@link AccessToken} with the result of a call to the {@literal tokeninfo} endpoint.
064     *
065     * @param rawInfo
066     *         raw response message.
067     * @param token
068     *         token identifier
069     * @param scopes
070     *         scopes of the token
071     * @param expiresAt
072     *         When this token will expires
073     */
074    public OpenAmAccessToken(final JsonValue rawInfo,
075                             final String token,
076                             final Set<String> scopes,
077                             final long expiresAt) {
078        this.rawInfo = rawInfo;
079        this.token = token;
080        this.scopes = Collections.unmodifiableSet(scopes);
081        this.expiresAt = expiresAt;
082    }
083
084    @Override
085    public Map<String, Object> getInfo() {
086        return rawInfo.asMap();
087    }
088
089    @Override
090    public JsonValue asJsonValue() {
091        return rawInfo;
092    }
093
094    @Override
095    public String getToken() {
096        return token;
097    }
098
099    @Override
100    public Set<String> getScopes() {
101        return scopes;
102    }
103
104    @Override
105    public long getExpiresAt() {
106        return expiresAt;
107    }
108
109    /**
110     * Build helper for {@link OpenAmAccessToken}.
111     */
112    public static class Builder {
113
114        private final TimeService time;
115
116        /**
117         * Creates a new Builder with the given {@link TimeService}.
118         *
119         * @param time time service used to compute the expiration date
120         */
121        public Builder(final TimeService time) {
122            this.time = time;
123        }
124
125        /**
126         * Builds a {@link OpenAmAccessToken} from a raw JSON response returned by the {@literal /oauth2/tokeninfo}
127         * endpoint.
128         *
129         * @param raw
130         *         JSON response
131         * @return a new {@link OpenAmAccessToken}
132         * @throws OAuth2TokenException
133         *         if the JSON response is not formatted correctly.
134         */
135        public OpenAmAccessToken build(final JsonValue raw) throws OAuth2TokenException {
136            try {
137                long expiresIn = raw.get("expires_in").required().asLong();
138                Set<String> scopes = new HashSet<String>(raw.get("scope").required().asList(String.class));
139                String token = raw.get("access_token").required().asString();
140                return new OpenAmAccessToken(raw,
141                                             token,
142                                             scopes,
143                                             getExpirationTime(expiresIn));
144            } catch (JsonValueException e) {
145                throw new OAuth2TokenException("Cannot build AccessToken from the given JSON: invalid format", e);
146            }
147        }
148
149        private long getExpirationTime(final long delayInSeconds) {
150            return time.now() + MILLISECONDS.convert(delayInSeconds, SECONDS);
151        }
152
153    }
154}