Skip to content

Commit 15cd7cd

Browse files
author
ychartois
committed
#71 - Migration from OpenID to OAuth
1 parent 120acaf commit 15cd7cd

File tree

7 files changed

+259
-81
lines changed

7 files changed

+259
-81
lines changed

app/controllers/Api.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import play.mvc.Controller;
1919
import play.mvc.Result;
2020
import repository.WorkshopRepository;
21-
import services.UserService;
2221

2322
import java.util.ArrayList;
2423
import java.util.HashMap;

app/controllers/Application.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@
1414
import views.html.workshops.alreadyPlayed;
1515
import views.html.workshops.newWorkshops;
1616

17+
import java.util.Map;
18+
1719

1820
/**
1921
* The main controller of the app which redirect to the main views
2022
*
2123
* @author ychartois
2224
*/
2325
public class Application extends Controller {
24-
26+
27+
28+
/**
29+
* We have to define a timeout when using WS.url().get(), we use the same for the all app
30+
*/
31+
public static final long TIMEOUT_WS = 5000l;
2532

2633
// <--------------------------------------------------------------------------->
2734
// - Actions Methods
@@ -124,4 +131,23 @@ public static String conf( String properties ) {
124131
return Play.application().configuration().getString(properties);
125132
}
126133

134+
/**
135+
* Convenient method to create a post body with WS.url()
136+
*
137+
* @param params map of all the post parameters
138+
*
139+
* @return a String that reduce the map
140+
*/
141+
public static String postParams(Map<String, String> params) {
142+
StringBuilder reduce = null;
143+
144+
for (String key : params.keySet()) {
145+
reduce = reduce == null ?
146+
new StringBuilder(key).append("=").append(params.get(key)) :
147+
reduce.append("&").append(key).append("=").append(params.get(key));
148+
}
149+
150+
return reduce != null ? reduce.toString() : "";
151+
}
152+
127153
}
Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
package controllers;
22

33
import com.avaje.ebean.Ebean;
4+
import com.fasterxml.jackson.databind.JsonNode;
45
import models.User;
56
import org.apache.commons.lang.StringUtils;
67
import play.Play;
78
import play.cache.Cache;
8-
import play.libs.F;
9-
import play.libs.OpenID;
9+
import play.libs.Crypto;
1010
import play.mvc.Controller;
1111
import play.mvc.Result;
12-
import services.UserService;
12+
import services.Google;
1313
import views.html.welcome.charter;
1414

15-
import java.util.HashMap;
16-
import java.util.Map;
15+
import java.net.MalformedURLException;
16+
import java.net.URISyntaxException;
1717

1818
/**
1919
* This controller got the action that allow the user to authenticate
2020
*
2121
* @author ychartois
2222
*/
2323
public class AuthenticationController extends Controller {
24+
25+
private static Google provider = new Google();
2426

2527
//<--------------------------------------------------------------------------->
2628
//- Actions(s)
@@ -30,23 +32,15 @@ public class AuthenticationController extends Controller {
3032
*
3133
* @return redirect on the verify service
3234
*/
33-
public static Result auth() {
34-
35-
// url are defined in Application.conf
36-
String providerUrl = Play.application().configuration().getString("openID.provider.url");
37-
String returnToUrl = Play.application().configuration().getString("openID.redirect.url");
35+
public static Result auth() throws MalformedURLException, URISyntaxException {
3836

39-
// We construct the OpenID map
40-
Map<String, String> attributes = new HashMap<>();
41-
attributes.put("Email", "http://schema.openid.net/contact/email");
42-
attributes.put("FirstName", "http://schema.openid.net/namePerson/first");
43-
attributes.put("LastName", "http://schema.openid.net/namePerson/last");
44-
attributes.put("Image", "http://schema.openid.net/media/image/48x48");
37+
// Generate a token
38+
String token = generateToken();
4539

46-
//We call the OpenID provider
47-
F.Promise<String> redirectUrl = OpenID.redirectURL(providerUrl, returnToUrl, attributes);
40+
// Get the endpoint of the provider
41+
provider.getEndpoint().get(Application.TIMEOUT_WS);
4842

49-
return redirect( redirectUrl.get() );
43+
return redirect( provider.authenticationUrl(token) );
5044
}
5145

5246
/**
@@ -55,35 +49,30 @@ public static Result auth() {
5549
* @return @return to welcome page or the charter agreement page
5650
*/
5751
public static Result verify() {
58-
// We get the OpenID info of the user
59-
F.Promise<OpenID.UserInfo> userInfoPromise = OpenID.verifiedId();
6052

61-
return verify( userInfoPromise.get() );
62-
}
53+
// We get the OpenID info of the user
54+
// We retrieve the user information
55+
JsonNode tokenInfo = provider.getTokenInfo( request() ).get( Application.TIMEOUT_WS );
56+
String token = provider.getToken( tokenInfo );
6357

58+
JsonNode userInfo = provider.getUserInfo( tokenInfo ).get(Application.TIMEOUT_WS);
6459

65-
/**
66-
* get the user from the database or cache or create it if it does not exist
67-
* this method is outside verify() to simplify tests ( OpenID.verifiedId() is not easy to test)
68-
*
69-
* @param userInfo userInfo given by the provider
70-
* @return @return to welcome page or the charter agreement page
71-
*/
72-
public static Result verify( OpenID.UserInfo userInfo ) {
7360
// If the user is not from the specified domain he can't connect
74-
if ( !StringUtils.endsWith( userInfo.attributes.get("Email"), Play.application().configuration().getString("mail.filter")) ) {
61+
if ( !StringUtils.endsWith( provider.getEmail(userInfo), Play.application().configuration().getString("mail.filter")) ) {
7562
return forbidden();
7663
}
7764

7865
// We add the authenticated user to the session
79-
session().put("email", userInfo.attributes.get("Email"));
66+
session().put("email", provider.getEmail(userInfo));
8067

8168
// We check if it's an existing user in base
8269
User user = Secured.getUser();
8370

8471
// if not we create it
8572
if ( user == null ) {
86-
user = new UserService().createNewUser(userInfo);
73+
user = provider.getUser(userInfo);
74+
user.charterAgree = false;
75+
user.picture = "/assets/images/avatar-default.png";
8776
}
8877

8978
if ( user.charterAgree ) {
@@ -112,4 +101,14 @@ public static Result logout() {
112101
return redirect(routes.Application.welcome());
113102
}
114103

104+
/**
105+
* Method that generate a token
106+
*
107+
* @return a unique token
108+
*/
109+
public static String generateToken() {
110+
String uuid = java.util.UUID.randomUUID().toString();
111+
return Crypto.sign(uuid);
112+
}
113+
115114
}

app/services/Google.java

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package services;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import controllers.Application;
5+
import models.User;
6+
import org.apache.http.client.methods.HttpGet;
7+
import org.apache.http.client.utils.URIBuilder;
8+
import play.libs.F;
9+
import play.libs.WS;
10+
import play.mvc.Http;
11+
12+
13+
import java.net.MalformedURLException;
14+
import java.net.URISyntaxException;
15+
import java.util.HashMap;
16+
import java.util.Map;
17+
18+
/**
19+
* @author yannig
20+
* <p/>
21+
* Date: 2014-06-24 3:56 PM
22+
*/
23+
public class Google {
24+
25+
private String authEndpoint;
26+
private String tokenEndpoint;
27+
private String userEndpoint;
28+
29+
30+
//<--------------------------------------------------------------------------->
31+
//- Authentication OAuth call
32+
//<--------------------------------------------------------------------------->
33+
34+
/**
35+
* <p/>
36+
* Document infos example:
37+
* <p/>
38+
* {
39+
* "issuer": "accounts.google.com",
40+
* "authorization_endpoint": "https://accounts.google.com/o/oauth2/auth",
41+
* "token_endpoint": "https://accounts.google.com/o/oauth2/token",
42+
* "userinfo_endpoint": "https://www.googleapis.com/plus/v1/people/me/openIdConnect",
43+
* "revocation_endpoint": "https://accounts.google.com/o/oauth2/revoke",
44+
* "jwks_uri": "https://www.googleapis.com/oauth2/v2/certs",
45+
* "response_types_supported": [
46+
* "code",
47+
* "token",
48+
* "id_token",
49+
* "code token",
50+
* "code id_token",
51+
* "token id_token",
52+
* "code token id_token",
53+
* "none"
54+
* ],
55+
* "subject_types_supported": [
56+
* "public"
57+
* ],
58+
* "id_token_alg_values_supported": [
59+
* "RS256"
60+
* ],
61+
* "token_endpoint_auth_methods_supported": [
62+
* "client_secret_post"
63+
* ]
64+
* }
65+
*/
66+
public F.Promise<JsonNode> getEndpoint() {
67+
68+
F.Promise<WS.Response> document = WS.url( Application.conf( "oauth.google.document" ) ).get();
69+
70+
return document.map( new F.Function<WS.Response, JsonNode>() {
71+
@Override
72+
public JsonNode apply( WS.Response response ) throws Throwable {
73+
74+
JsonNode jsonNode = response.asJson();
75+
authEndpoint = jsonNode.get( "authorization_endpoint" ).asText();
76+
tokenEndpoint = jsonNode.get( "token_endpoint" ).asText();
77+
userEndpoint = jsonNode.get( "userinfo_endpoint" ).asText();
78+
79+
return jsonNode;
80+
}
81+
} );
82+
83+
}
84+
85+
/**
86+
*/
87+
public String authenticationUrl( String token ) throws URISyntaxException, MalformedURLException {
88+
HttpGet endpoint = new HttpGet( authEndpoint );
89+
URIBuilder builder = new URIBuilder( endpoint.getURI() );
90+
builder.addParameter( "client_id", Application.conf("oauth.google.clientID") )
91+
.addParameter( "response_type", Application.conf( "oauth.google.responseType" ) )
92+
.addParameter( "scope", Application.conf( "oauth.google.scope" ) )
93+
.addParameter( "redirect_uri", Application.conf( "oauth.verifyUri" ) )
94+
.addParameter( "state", token );
95+
96+
return builder.build().toURL().toString();
97+
}
98+
99+
/**
100+
* <p/>
101+
* Token info JSON example:
102+
* {
103+
* "access_token": "ya29.1.AADtN_UoDvNkY-6HzKuXNyMSgJjrZa6ToKBVaLGeDnRLHMjM1SiOw7c3Ci7Nsk20vuVyaJ4",
104+
* "token_type": "Bearer",
105+
* "expires_in": 3600,
106+
* "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjAwYzVmZjE3NzQ4OTAyYmY3MzI5YmIyNDRiZmViZmZlMDg2MGY3M2IifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTA0NTczNjQyMjcyNzQ0MjE5OTkxIiwiYXpwIjoiMjE2OTg2Mjc5MDUwLWQycTRpNW1ybGVpbmRnOGV2NWF0ZDZ0OTlvczBtZXY3LmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiZW1haWwiOiJ5Y2hhcnRvaXNAeXNkZXYuZnIiLCJhdF9oYXNoIjoiVjNLa0pFMl9RV1pEM1RQS1kyT2dOdyIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdWQiOiIyMTY5ODYyNzkwNTAtZDJxNGk1bXJsZWluZGc4ZXY1YXRkNnQ5OW9zMG1ldjcuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJoZCI6InlzZGV2LmZyIiwiaWF0IjoxMzk4ODUwMDYyLCJleHAiOjEzOTg4NTM5NjJ9.ezk20Jf28gcsRPu4bRqec4Y4QIMeM89NAFXYCnnQ4vg836RB0ooMoNb9kgvhRXy2OcZrpsaDe1wsPBtg9_9oA4-PcncGeIUsM5mVcQ5qlqW5AADBhNT2L5gLTXQIlc-3aNVDt4XjZ794Q_QW_DDtrajsrVi2Alop1OUQf_gxBy8"
107+
* }
108+
*/
109+
public F.Promise<JsonNode> getTokenInfo( Http.Request request ) {
110+
String code = request.queryString().get( "code" )[0];
111+
112+
Map<String, String> postParams = new HashMap<>();
113+
postParams.put( "code", code );
114+
postParams.put( "client_id", Application.conf( "oauth.google.clientID" ) );
115+
postParams.put( "client_secret", Application.conf( "oauth.google.secret" ) );
116+
postParams.put( "grant_type", Application.conf( "oauth.google.grant.type" ) );
117+
postParams.put( "redirect_uri", Application.conf( "oauth.verifyUri" ) );
118+
119+
F.Promise<WS.Response> tokenRequest = WS.url( tokenEndpoint )
120+
.setHeader( "Content-Type", "application/x-www-form-urlencoded" )
121+
.post( Application.postParams( postParams ) );
122+
123+
return tokenRequest.map( new F.Function<WS.Response, JsonNode>() {
124+
@Override
125+
public JsonNode apply( WS.Response response ) throws Throwable {
126+
return response.asJson();
127+
}
128+
} );
129+
}
130+
131+
/**
132+
* <p/>
133+
* User info JSON example:
134+
* {
135+
* "kind": "plus#personOpenIdConnect",
136+
* "sub": "1087545455544219991",
137+
* "name": "Yannick Chartois",
138+
* "given_name": "Yannick",
139+
* "family_name": "Chartois",
140+
* "picture": "https:https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg?sz=50",
141+
* "email": "[email protected]",
142+
* "email_verified": "true",
143+
* "locale": "fr",
144+
* "hd": "ysdev.fr"
145+
* }
146+
*/
147+
public F.Promise<JsonNode> getUserInfo( JsonNode tokenInfo ) {
148+
F.Promise<WS.Response> userInfos = WS.url( userEndpoint )
149+
.setQueryParameter( "access_token", getToken( tokenInfo ) )
150+
.get();
151+
152+
return userInfos.map( new F.Function<WS.Response, JsonNode>() {
153+
@Override
154+
public JsonNode apply( WS.Response response ) throws Throwable {
155+
return response.asJson();
156+
}
157+
} );
158+
}
159+
160+
//<--------------------------------------------------------------------------->
161+
//- Convenient method to retrieve information
162+
//<--------------------------------------------------------------------------->
163+
164+
public User getUser( JsonNode userInfo ) {
165+
166+
User user = new User();
167+
user.firstName = userInfo.get( "given_name" ).asText();
168+
user.lastName = userInfo.get( "family_name" ).asText();
169+
user.email = userInfo.get( "email" ).asText();
170+
171+
return user;
172+
}
173+
174+
175+
public String getEmail( JsonNode userInfo ) {
176+
return userInfo.get( "email" ).asText();
177+
}
178+
179+
public String getToken( JsonNode tokenInfo ) {
180+
return tokenInfo.get( "access_token" ).asText();
181+
}
182+
}

0 commit comments

Comments
 (0)