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