001/* 002 * Stallion Core: A Modern Web Framework 003 * 004 * Copyright (C) 2015 - 2016 Stallion Software LLC. 005 * 006 * This program is free software: you can redistribute it and/or modify it under the terms of the 007 * GNU General Public License as published by the Free Software Foundation, either version 2 of 008 * the License, or (at your option) any later version. This program is distributed in the hope that 009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 011 * License for more details. You should have received a copy of the GNU General Public License 012 * along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>. 013 * 014 * 015 * 016 */ 017 018package io.stallion.users; 019 020import io.stallion.exceptions.ClientException; 021import io.stallion.exceptions.RedirectException; 022import io.stallion.restfulEndpoints.BodyParam; 023import io.stallion.restfulEndpoints.MinRole; 024import io.stallion.settings.Settings; 025import io.stallion.templating.TemplateRenderer; 026import io.stallion.requests.validators.ParamExtractor; 027import org.apache.commons.lang3.StringUtils; 028 029import javax.ws.rs.*; 030 031import java.io.UnsupportedEncodingException; 032import java.net.URLEncoder; 033import java.util.*; 034 035import static io.stallion.utils.Literals.*; 036import static io.stallion.Context.*; 037 038 039public class OAuthEndpoints { 040 041 @GET 042 @Path("/auth") 043 @Produces("text/html") 044 @MinRole(Role.CONTACT) 045 public Object authScreen(@QueryParam("client_id") String clientFullId, @QueryParam("scopes") String scopesString) { 046 String[] scopes = StringUtils.split(scopesString, ","); 047 String description = ""; 048 Map<String, String> descriptionMap = Settings.instance().getoAuth().getScopeDescriptions(); 049 for (int x = 0; x<scopes.length; x++) { 050 String scope = scopes[x]; 051 String s = or(descriptionMap.getOrDefault(scope, scope), scope); 052 if (scopes.length == 1) { 053 description = s; 054 } else if (scopes.length == 2) { 055 if (x == 0) { 056 description += s; 057 } else { 058 description += " and " + s; 059 } 060 } else if (x + 1 == scopes.length) { 061 description += " and " + s; 062 } else { 063 description += "," + s; 064 } 065 } 066 Map<String, Object> ctx = map(val("clientId", clientFullId)); 067 ctx.put("client", OAuthClientController.instance().clientForFullId(clientFullId)); 068 ctx.put("scopesDescription", description); 069 ctx.put("scopes", set(scopes)); 070 String html = TemplateRenderer.instance().renderTemplate("stallion:public/oauth.jinja"); 071 return html; 072 } 073 074 @POST 075 @Path("/authorize-and-redirect") 076 @MinRole(Role.CONTACT) 077 @Consumes("application/x-www-form-urlencoded") 078 public void authorizeToRedirect(@BodyParam("clientId") String fullClientId, @BodyParam("redirectUri") String redirectUri, @BodyParam("scopes") String scopesString, @BodyParam("state") String state) { 079 state = or(state, ""); 080 boolean scoped = true; 081 Set<String> scopes = null; 082 if (scopesString == null) { 083 scoped = false; 084 scopes = set(); 085 } else { 086 scopes = set(or(scopesString, "").split(",")); 087 } 088 String providedCode = request().getBodyMap().getOrDefault("providedCode", "").toString(); 089 OAuthApproval approval = OAuthApprovalController.instance().checkGrantApprovalForUser(GrantType.CODE, getUser(), fullClientId, scopes, scoped, redirectUri, providedCode); 090 if (!redirectUri.contains("?")) { 091 redirectUri += "?"; 092 } else if (!redirectUri.endsWith("&")) { 093 redirectUri += "&"; 094 } 095 try { 096 redirectUri += "code=" + approval.getCode() + "&state=" + URLEncoder.encode(state, "utf-8"); 097 } catch (UnsupportedEncodingException e) { 098 throw new RuntimeException(e); 099 } 100 ; 101 throw new RedirectException(redirectUri, 302); 102 } 103 104 @POST 105 @Path("/authorize-and-redirect-to-hash") 106 @MinRole(Role.CONTACT) 107 @Consumes("application/x-www-form-urlencoded") 108 public void authorizeToRedirectHash(@BodyParam("clientId") String fullClientId, @BodyParam("redirectUri") String redirectUri, @BodyParam("scopes") String scopesString, @BodyParam("state") String state) { 109 state = or(state, ""); 110 boolean scoped = true; 111 Set<String> scopes = null; 112 if (scopesString == null) { 113 scoped = false; 114 scopes = set(); 115 } else { 116 scopes = set(or(scopesString, "").split(",")); 117 } 118 OAuthApproval approval = OAuthApprovalController.instance().checkGrantApprovalForUser(GrantType.TOKEN, getUser(), fullClientId, scopes, scoped, redirectUri); 119 if (!redirectUri.contains("#")) { 120 redirectUri += "#"; 121 } else if (!redirectUri.endsWith("&")) { 122 redirectUri += "&"; 123 } 124 try { 125 redirectUri += "token=" + approval.getAccessToken() + "&state=" + URLEncoder.encode(state, "utf-8"); 126 } catch (UnsupportedEncodingException e) { 127 throw new RuntimeException(e); 128 } 129 throw new RedirectException(redirectUri, 302); 130 } 131 132 @POST 133 @Path("/authorize-to-json") 134 @MinRole(Role.CONTACT) 135 public Map<String, Object> authorize(@BodyParam("grantType") String grantTypeString, @BodyParam("clientId") String fullClientId, @BodyParam("redirectUri") String redirectUri, @BodyParam("scopes") String scopesString, @BodyParam("state") String state) { 136 GrantType grantType = Enum.valueOf(GrantType.class, grantTypeString.toUpperCase()); 137 state = or(state, ""); 138 boolean scoped = true; 139 Set<String> scopes = null; 140 if (scopesString == null) { 141 scoped = false; 142 scopes = set(); 143 } else { 144 scopes = set(or(scopesString, "").split(",")); 145 } 146 OAuthApproval approval = OAuthApprovalController.instance().checkGrantApprovalForUser(grantType, getUser(), fullClientId, scopes, scoped, redirectUri); 147 Map<String, Object> data = map(val("state", state), val("redirect_uri", redirectUri)); 148 if (grantType.equals(GrantType.CODE)) { 149 data.put("code", approval.getCode()); 150 } else if (grantType.equals(GrantType.TOKEN)) { 151 data.put("access_token", approval.getAccessToken()); 152 } 153 return data; 154 } 155 156 @POST 157 @Path("/refresh") 158 public Object refresh( 159 @BodyParam("access_token") String accessToken, 160 @BodyParam("refresh_token") String refreshToken, 161 @BodyParam("client_id") String fullClientId, 162 @BodyParam("client_secret") String clientSecret ) { 163 164 OAuthApproval approval = OAuthApprovalController.instance().newAccessTokenForRefreshToken(refreshToken, accessToken, fullClientId, clientSecret); 165 return map(val("access_token", approval.getAccessToken()), val("refresh_token", approval.getRefreshToken())); 166 } 167 168 169 @POST 170 @Path("/token") 171 public Object grantToken(@BodyParam("grant_type") String grantType) { 172 switch(grantType) { 173 case "authorization_code": 174 return authorizationCodeGrantToken(); 175 case "password": 176 return passwordGrantToken(); 177 default: 178 throw new ClientException("Could not understand grant_type: " + grantType); 179 } 180 } 181 182 public Object authorizationCodeGrantToken() { 183 ParamExtractor<String> params = 184 new ParamExtractor(request().getBodyMap(), 185 "Required post body parameter {0} was not found."); 186 String code = params.get("code"); 187 String redirectUri = params.get("redirect_uri"); 188 String fullClientId = params.get("client_id"); 189 String clientSecret = params.get("client_secret"); 190 OAuthClient client = OAuthClientController.instance().clientForFullId(fullClientId); 191 if (emptyInstance(client)) { 192 throw new ClientException("Client not found with id :'" + fullClientId + "'"); 193 } 194 if (client.hasGrantType(GrantType.CODE)) { 195 throw new ClientException("Client cannot use password login."); 196 } 197 if (client.isRequiresSecret() && !clientSecret.equals(client.getClientSecret())) { 198 throw new ClientException("The client secret was not valid"); 199 } 200 if (!client.getAllowedRedirectUris().contains(redirectUri)) { 201 throw new ClientException("The URI '" + redirectUri + "' was not on the allowed list."); 202 } 203 OAuthApproval token = OAuthApprovalController.instance().forUniqueKey("code", code); 204 if (emptyInstance(token)) { 205 throw new ClientException("No valid token found for code: '" + code + "'"); 206 } 207 if (token.isVerified()) { 208 throw new ClientException("Code has already been used: '" + code + "'"); 209 } 210 // Tokens expire in 15 minutes if they are not verified 211 if ((token.getCreatedAt() + (15 * 60 * 1000)) < mils() ) { 212 throw new ClientException("Code was not verified within fifteen minutes: '" + code + "'"); 213 } 214 token.setVerified(true); 215 token.setCode(UUID.randomUUID().toString()); 216 OAuthApprovalController.instance().save(token); 217 218 return map( 219 val("access_token", token.getAccessToken()), 220 val("refresh_token", token.getRefreshToken()) 221 ); 222 223 224 } 225 226 public Object passwordGrantToken() { 227 ParamExtractor<String> params = 228 new ParamExtractor(request().getBodyMap(), 229 "Required post body paramater {0} was not found."); 230 String password = params.get("password"); 231 String username = params.get("username"); 232 String clientId = params.get("clientId"); 233 OAuthClient client = OAuthClientController.instance().forUniqueKey("clientKey", clientId); 234 if (emptyInstance(client)) { 235 throw new ClientException("Client not found with id :'" + clientId + "'"); 236 } 237 if (client.hasGrantType(GrantType.PASSWORD)) { 238 throw new ClientException("Client cannot use password login."); 239 } 240 Set<String> scopes; 241 boolean scoped = true; 242 if (client.isScoped()) { 243 scopes = new HashSet<>(client.getScopes()); 244 scoped = true; 245 } else { 246 scopes = set(); 247 scoped = false; 248 } 249 IUser user = UserController.instance().checkUserLoginValid(username, password); 250 OAuthApproval token = OAuthApprovalController.instance().generateNewApprovalForUser(user, client, scopes, scoped, ""); 251 return map(val("access_token", token.getAccessToken())); 252 } 253 254 255}