/
OAuth2Provider.scala
172 lines (145 loc) · 5.57 KB
/
OAuth2Provider.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package services.auth
import java.util.UUID.randomUUID
import scala.concurrent.Await
import scala.concurrent.duration.DurationInt
import models.command.NewToken
import models.user.SkimboToken
import play.api.Logger
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.ws.WS
import play.api.mvc.Call
import play.api.mvc.RequestHeader
import play.api.mvc.Result
import services.dao.UserDao
import services.post.Poster
import scala.util.Success
import scala.util.Failure
import services.star.Starer
import scala.concurrent.Future
trait OAuth2Provider extends AuthProvider {
// OAuth2 provider settings (override these)
def method: Verb = Get
def accessTokenHeaders: Seq[(String, String)] = Seq.empty
// Define how to get token and expires timestamp from WS response
def processToken(response: play.api.libs.ws.Response): Token
def additionalAccreditationParameters: Map[String, String] = Map.empty
// Session and cookies fields
lazy val fieldCsrf = namespace + "_csrf"
lazy val fieldToken = namespace + "_token"
lazy val fieldExpires = namespace + "_expires"
// Load infos (token, secret, urls) from configuration
lazy val clientId = config.getString("clientId").get
lazy val secret = config.getString("secret").get
lazy val authorizeUrl = config.getString("authorize").get
lazy val accessTokenUrl = config.getString("accessToken").get
/**
* Execute authentication on provider
* @param redirectRoute : Where the user wil be redirected after correct authentication
*/
override def auth(redirectRoute: Call)(implicit request: RequestHeader): Result = {
request.getQueryString("code") match {
case None => redirectToAccreditationPage // Step one : redirect the user to the service's accreditation page
case Some(code) => {
retrieveAccessToken(code, redirectRoute) // Step two : Retrieve authentication token from WS
}
}
}
/**
* Shortcut to make WS request without passing token as parameter
* TOKEN must be in session
*/
override def fetch(idUser: String, url: String) = {
WS.url(url).withQueryString("access_token" -> getToken(idUser).get.token)
}
/**
* Get token from session if exists
*/
override def getToken(idUser: String) = {
Await.result(UserDao.getToken(idUser, this), 10 second)
}
/**
* Redirect the user to the service's accreditation page
*/
private def redirectToAccreditationPage(implicit request: RequestHeader) = {
val csrf = randomUUID().toString
val redirectQueryString = Map(
"client_id" -> clientId,
"redirect_uri" -> authRoute.absoluteURL(false),
"state" -> csrf,
"scope" -> permissions.mkString(permissionsSep),
"response_type" -> "code")
val url = (redirectQueryString ++ additionalAccreditationParameters)
.foldLeft(authorizeUrl + "?")((url, qs) => url + qs._1 + "=" + qs._2 + "&")
Logger("OAuth2provider").info("URL => "+url)
Redirect(url).withSession(request.session + (fieldCsrf -> csrf))
}
/**
* Check authentication code and fetch accessToken from provider WS
*/
private def retrieveAccessToken(code: String, redirectRoute: Call)(implicit request: RequestHeader) = {
// Check if CSRF fields are ok
val sess_csrf = request.session.get(fieldCsrf)
val verif_csrf = request.getQueryString("state")
if (sess_csrf.isDefined && sess_csrf == verif_csrf) {
// Send request to retrieve auth token and parse response
Async {
fetchAccessTokenResponse(code).flatMap(response =>
processTokenSafe(response) match {
// Provider return token
case Token(Some(token), _, refresh) => {
startUser(SkimboToken(token, refresh), redirectRoute)
}
// Provider return nothing > an error has occurred during authentication
case _ =>
Future(Redirect(redirectRoute).flashing("login-error" -> name))
})
}
// CRSF attack detected
} else {
Logger.warn("CSRF attack detected from IP %s" format (request.remoteAddress))
Redirect(redirectRoute).flashing("login-error" -> "security")
}
}
/**
* Call provider WS to fetch token string (json or queryString)
*/
protected def fetchAccessTokenResponse(code: String)(implicit request: RequestHeader) = {
val data = Map(
"client_id" -> clientId,
"client_secret" -> secret,
"code" -> code,
"redirect_uri" -> authRoute.absoluteURL(false),
"grant_type" -> "authorization_code")
val req = WS.url(accessTokenUrl).withHeaders(accessTokenHeaders: _*)
Logger.info("OAuth fetch token ==> " + data)
method match {
case Get => req.withQueryString(data.toSeq: _*).get
case Post => req.post(data.mapValues(Seq(_)))
}
}
protected def processTokenSafe(response: play.api.libs.ws.Response): Token = {
try {
processToken(response)
} catch {
case _: Throwable => {
Logger.error("[" + name + "] Unable to process token")
Logger.info(response.body)
Token(None, None)
}
}
}
override def post(idUser: String, url:String, queryString:Seq[(String, String)], headers:Seq[(String, String)], content:String) = {
val queryS = queryString ++ Seq("access_token" -> getToken(idUser).get.token)
WS.url(url)
.withQueryString( queryS:_* )
.withHeaders(headers:_*)
.post(content)
}
}
/**
* Token send from provider
*/
sealed case class Token(token: Option[String], expires: Option[Int], refreshToken: Option[String] = None)
sealed abstract class Verb
case object Get extends Verb
case object Post extends Verb