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}