@@ -80,14 +80,16 @@ trait AuthActions {
8080 new Google2FAGroupChecker (_, panDomainSettings.s3BucketLoader, applicationName)
8181 }
8282
83- /**
84- * A cookie key that stores the target URL that was being accessed when redirected for authentication
85- */
86- val LOGIN_ORIGIN_KEY = " panda-loginOriginUrl"
83+ /*
84+ * A cookie key that stores the target URL that was being accessed when redirected for authentication.
85+ * Takes a session ID to allow multiple tabs to reauth concurrently without conflict.
86+ */
87+ def loginOriginKey (sessionId : String ) = s " panda-loginOriginUrl- $sessionId"
8788 /*
8889 * Cookie key containing an anti-forgery token; helps to validate that the oauth callback arrived in response to the correct oauth request
90+ * Takes a session ID to allow multiple tabs to reauth concurrently without conflict.
8991 */
90- val ANTI_FORGERY_KEY = " panda-antiForgeryToken"
92+ def antiForgeryTokenKey ( sessionId : String ) = s " panda-antiForgeryToken- $sessionId "
9193 /*
9294 * Cookie that will make panda behave as if the cookie has expired.
9395 * NOTE: This cookie is for debugging only! It should _not_ be set by any application code to expire the cookie!! Use the `processLogout` action instead!!
@@ -98,16 +100,17 @@ trait AuthActions {
98100 Cookie (
99101 name,
100102 value = URLEncoder .encode(value, " UTF-8" ),
103+ maxAge = Some (300 ), // 5 mins - plenty of time for auth to complete
101104 secure = true ,
102105 httpOnly = true ,
103106 // Chrome will pass back SameSite=Lax cookies, but Firefox requires
104107 // SameSite=None, since the cookies are to be returned on a redirect
105108 // from a 3rd party
106109 sameSite = Some (Cookie .SameSite .None )
107110 )
108- private lazy val discardCookies = Seq (
109- DiscardingCookie (LOGIN_ORIGIN_KEY , secure = true ),
110- DiscardingCookie (ANTI_FORGERY_KEY , secure = true ),
111+ private def discardCookies ( sessionId : String ) = Seq (
112+ DiscardingCookie (loginOriginKey(sessionId) , secure = true ),
113+ DiscardingCookie (antiForgeryTokenKey(sessionId) , secure = true ),
111114 DiscardingCookie (FORCE_EXPIRY_KEY , secure = true )
112115 )
113116
@@ -116,10 +119,14 @@ trait AuthActions {
116119 * but if you want to show welcome page with a button on it then override.
117120 */
118121 def sendForAuth (implicit request : RequestHeader , email : Option [String ] = None ) = {
122+ val sessionId = OAuth .generateSessionId()
119123 val antiForgeryToken = OAuth .generateAntiForgeryToken()
120- OAuth .redirectToOAuthProvider(antiForgeryToken, email)(ec) map { res =>
124+ OAuth .redirectToOAuthProvider(sessionId, antiForgeryToken, email)(ec) map { res =>
121125 val originUrl = request.uri
122- res.withCookies(cookie(ANTI_FORGERY_KEY , antiForgeryToken), cookie(LOGIN_ORIGIN_KEY , originUrl))
126+ res.withCookies(
127+ cookie(antiForgeryTokenKey(sessionId), antiForgeryToken),
128+ cookie(loginOriginKey(sessionId), originUrl)
129+ ).discardingHeader(FORCE_EXPIRY_KEY )
123130 }
124131 }
125132
@@ -150,14 +157,24 @@ trait AuthActions {
150157 def invalidUserMessage (claimedAuth : AuthenticatedUser ) = s " user ${claimedAuth.user.email} not valid for $system"
151158
152159 private def decodeCookie (name : String )(implicit request : RequestHeader ) =
153- request.cookies.get(name).map(cookie => URLDecoder .decode(cookie.value, " UTF-8" ))
160+ request.cookies.get(name).map(cookie => URLDecoder .decode(cookie.value, " UTF-8" )).toRight(
161+ left = BadRequest (s " missing cookie $name" )
162+ )
163+
164+ private def fetchSessionIdFromState ()(implicit request : RequestHeader ) =
165+ request.getQueryString(" state" ) match {
166+ case Some (s " $sessionId+ $_" ) => Right (sessionId)
167+ case Some (_) => Left (BadRequest (" State parameter returned missing a session ID" ))
168+ case None => Left (BadRequest (" No state parameter passed in callback" ))
169+ }
154170
155171 def processOAuthCallback ()(implicit request : RequestHeader ): Future [Result ] = {
156172 (for {
157- token <- decodeCookie(ANTI_FORGERY_KEY )
158- originalUrl <- decodeCookie(LOGIN_ORIGIN_KEY )
173+ sessionId <- fetchSessionIdFromState()
174+ token <- decodeCookie(antiForgeryTokenKey(sessionId))
175+ originalUrl <- decodeCookie(loginOriginKey(sessionId))
159176 } yield {
160- OAuth .validatedUserIdentity(token)(request, ec, wsClient).map { claimedAuth =>
177+ OAuth .validatedUserIdentity(sessionId, token)(request, ec, wsClient).map { claimedAuth =>
161178 val existingAuthenticatedIn = readAuthenticatedUser(request).map(_.authenticatedIn)
162179 val authedUserData =
163180 claimedAuth.copy(
@@ -170,13 +187,14 @@ trait AuthActions {
170187 val updatedCookie = generateCookie(authedUserData)
171188 Redirect (originalUrl)
172189 .withCookies(updatedCookie)
173- .discardingCookies(discardCookies: _* )
190+ .discardingCookies(discardCookies(sessionId) : _* )
174191 } else {
175192 showUnauthedMessage(invalidUserMessage(authedUserData))
176193 }
177194 }
178- }) getOrElse {
179- Future .successful(BadRequest (" Missing cookies" ))
195+ }) match {
196+ case Left (failure) => Future .successful(failure)
197+ case Right (eventualSuccess) => eventualSuccess
180198 }
181199 }
182200
0 commit comments