001/*
002 * (C) Copyright 2014-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Arnaud Kervern
018 */
019package org.nuxeo.ecm.platform.oauth2.clients;
020
021import static java.util.Objects.requireNonNull;
022import static java.util.Objects.requireNonNullElse;
023import static org.nuxeo.ecm.platform.oauth2.clients.OAuth2ClientService.OAUTH2CLIENT_SCHEMA;
024
025import java.util.Arrays;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.regex.Pattern;
030
031import org.apache.commons.lang3.StringUtils;
032import org.nuxeo.ecm.core.api.DocumentModel;
033import org.nuxeo.ecm.directory.BaseSession;
034import org.nuxeo.runtime.api.Framework;
035
036/**
037 * @author <a href="mailto:ak@nuxeo.com">Arnaud Kervern</a>
038 * @since 5.9.2
039 */
040public class OAuth2Client {
041
042    /**
043     * @since 11.1
044     */
045    public static final String NAME_FIELD = "name";
046
047    /**
048     * @since 11.1
049     */
050    public static final String ID_FIELD = "clientId";
051
052    /**
053     * @since 11.1
054     */
055    public static final String SECRET_FIELD = "clientSecret";
056
057    /**
058     * @since 11.1
059     */
060    public static final String REDIRECT_URI_FIELD = "redirectURIs";
061
062    /**
063     * @since 11.1
064     */
065    public static final String AUTO_GRANT_FIELD = "autoGrant";
066
067    /**
068     * @since 11.1
069     */
070    public static final String ENABLED_FIELD = "enabled";
071
072    /**
073     * @since 11.1
074     */
075    public static final String REDIRECT_URI_SEPARATOR = ",";
076
077    protected static final Pattern LOCALHOST_PATTERN = Pattern.compile("http://localhost(:\\d+)?(/.*)?");
078
079    protected String name;
080
081    protected String id;
082
083    protected String secret;
084
085    /**
086     * @since 9.2
087     */
088    protected List<String> redirectURIs;
089
090    /**
091     * @since 9.10
092     */
093    protected boolean autoGrant;
094
095    protected boolean enabled;
096
097    /**
098     * @since 9.10
099     */
100    protected OAuth2Client(String name, String id, String secret, List<String> redirectURIs, boolean autoGrant,
101            boolean enabled) {
102        this.name = name;
103        this.id = id;
104        this.secret = secret;
105        this.redirectURIs = redirectURIs;
106        this.autoGrant = autoGrant;
107        this.enabled = enabled;
108    }
109
110    public String getName() {
111        return name;
112    }
113
114    public String getId() {
115        return id;
116    }
117
118    /**
119     * @since 9.2
120     */
121    public List<String> getRedirectURIs() {
122        return redirectURIs;
123    }
124
125    /**
126     * @since 9.10
127     */
128    public boolean isAutoGrant() {
129        return autoGrant;
130    }
131
132    public boolean isEnabled() {
133        return enabled;
134    }
135
136    /**
137     * @since 11.1
138     */
139    public String getSecret() {
140        return secret;
141    }
142
143    public static OAuth2Client fromDocumentModel(DocumentModel doc) {
144        String name = (String) doc.getProperty(OAUTH2CLIENT_SCHEMA, NAME_FIELD);
145        String id = (String) doc.getProperty(OAUTH2CLIENT_SCHEMA, ID_FIELD);
146        boolean autoGrant = requireNonNullElse((Boolean) doc.getProperty(OAUTH2CLIENT_SCHEMA, AUTO_GRANT_FIELD), false);
147        boolean enabled = requireNonNullElse((Boolean) doc.getProperty(OAUTH2CLIENT_SCHEMA, ENABLED_FIELD), false);
148        String redirectURIsProperty = requireNonNullElse(
149                (String) doc.getProperty(OAUTH2CLIENT_SCHEMA, REDIRECT_URI_FIELD), StringUtils.EMPTY);
150        List<String> redirectURIs = Arrays.asList(StringUtils.split(redirectURIsProperty, REDIRECT_URI_SEPARATOR));
151        String secret = (String) doc.getProperty(OAUTH2CLIENT_SCHEMA, SECRET_FIELD);
152
153        return new OAuth2Client(name, id, secret, redirectURIs, autoGrant, enabled);
154    }
155
156    /**
157     * A redirect URI is considered as valid if and only if:
158     * <ul>
159     * <li>It is not empty</li>
160     * <li>It starts with https, e.g. https://my.redirect.uri</li>
161     * <li>It doesn't start with http, e.g. nuxeo://authorize</li>
162     * <li>It starts with http://localhost with localhost not part of the domain name, e.g. http://localhost:8080/nuxeo,
163     * a counter-example being http://localhost.somecompany.com</li>
164     * <li>The Nuxeo node is in Dev mode</li>
165     * </ul>
166     *
167     * @since 9.2
168     */
169    public static boolean isRedirectURIValid(String redirectURI) {
170        String trimmed = redirectURI.trim();
171        return !trimmed.isEmpty() && (trimmed.startsWith("https") || !trimmed.startsWith("http")
172                || LOCALHOST_PATTERN.matcher(trimmed).matches() || Framework.isDevModeSet());
173    }
174
175    public boolean isValidWith(String clientId, String clientSecret) {
176        // Related to RFC 6749 2.3.1 clientSecret is omitted if empty
177        return enabled && id.equals(clientId) && (StringUtils.isEmpty(secret) || secret.equals(clientSecret));
178    }
179
180    /**
181     * Creates a {@link DocumentModel} from an {@link OAuth2Client}.
182     *
183     * @param oAuth2Client the {@code OAuth2Client} to convert
184     * @return the {@code DocumentModel} corresponding to the {@code OAuth2Client}
185     * @since 11.1
186     */
187    public static DocumentModel fromOAuth2Client(OAuth2Client oAuth2Client) {
188        return BaseSession.createEntryModel(null, OAUTH2CLIENT_SCHEMA, null, toMap(oAuth2Client));
189    }
190
191    /**
192     * Updates the {@link DocumentModel} by the {@link OAuth2Client}.
193     *
194     * @param documentModel the document model to update
195     * @param oAuth2Client the new values of document
196     * @return the updated {@code DocumentModel}
197     * @throws NullPointerException if the documentModel or oAuth2Client is {@code null}
198     * @since 11.1
199     */
200    public static DocumentModel updateDocument(DocumentModel documentModel, OAuth2Client oAuth2Client) {
201        requireNonNull(documentModel, "documentModel model is required");
202        documentModel.setProperties(OAUTH2CLIENT_SCHEMA, OAuth2Client.toMap(oAuth2Client));
203        return documentModel;
204    }
205
206    /**
207     * Converts an {@link OAuth2Client} to map structure.
208     *
209     * @param oAuth2Client the {@code OAuth2Client}
210     * @return a map representing the {@code OAuth2Client}
211     * @since 11.1
212     */
213    public static Map<String, Object> toMap(OAuth2Client oAuth2Client) {
214        Map<String, Object> values = new HashMap<>();
215        values.put(NAME_FIELD, oAuth2Client.getName());
216        values.put(ID_FIELD, oAuth2Client.getId());
217        values.put(REDIRECT_URI_FIELD, StringUtils.join(oAuth2Client.getRedirectURIs(), REDIRECT_URI_SEPARATOR));
218        values.put(AUTO_GRANT_FIELD, oAuth2Client.isAutoGrant());
219        values.put(ENABLED_FIELD, oAuth2Client.isEnabled());
220        if (StringUtils.isNotEmpty(oAuth2Client.getSecret())) {
221            values.put(SECRET_FIELD, oAuth2Client.getSecret());
222        }
223        return values;
224    }
225
226    /**
227     * @since 9.2
228     */
229    @Override
230    public String toString() {
231        return String.format("%s(name=%s, id=%s, redirectURIs=%s, autoGrant=%b, enabled=%b)",
232                getClass().getSimpleName(), name, id, redirectURIs, autoGrant, enabled);
233    }
234}