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}