Skip to content

Commit 27d024c

Browse files
committed
refacto session traits and objects
1 parent e9d6041 commit 27d024c

File tree

10 files changed

+230
-230
lines changed

10 files changed

+230
-230
lines changed

session/core/src/main/scala/app/softnetwork/session/service/SessionEndpoints.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,17 @@ trait SessionEndpoints extends TapirEndpoints {
2424

2525
import TapirSessionOptions._
2626

27-
lazy val oneOffSession: SessionContinuityEndpoints[Session] = oneOff
27+
lazy val oneOffSession: TapirSessionContinuity[Session] = oneOff
2828

29-
lazy val refreshableSession: SessionContinuityEndpoints[Session] = refreshable
29+
lazy val refreshableSession: TapirSessionContinuity[Session] = refreshable
3030

3131
import TapirCsrfOptions._
3232

3333
lazy val checkHeaderMode: TapirCsrfCheckMode[Session] = checkHeader
3434

3535
lazy val checkHeaderAndFormMode: TapirCsrfCheckMode[Session] = checkHeaderAndForm
3636

37-
lazy val sc: SessionContinuityEndpoints[Session] = oneOffSession
37+
lazy val sc: TapirSessionContinuity[Session] = oneOffSession
3838

3939
lazy val st: SetSessionTransport = CookieST
4040

@@ -52,28 +52,28 @@ trait SessionEndpoints extends TapirEndpoints {
5252

5353
case class OneOffCookieSessionEndpoints(system: ActorSystem[_], checkHeaderAndForm: Boolean)
5454
extends SessionEndpoints {
55-
override lazy val sc: SessionContinuityEndpoints[Session] = oneOffSession
55+
override lazy val sc: TapirSessionContinuity[Session] = oneOffSession
5656
override lazy val st: SetSessionTransport = CookieST
5757
override lazy val checkMode: TapirCsrfCheckMode[Session] = checkHeaderAndForm
5858
}
5959

6060
case class OneOffHeaderSessionEndpoints(system: ActorSystem[_], checkHeaderAndForm: Boolean)
6161
extends SessionEndpoints {
62-
override lazy val sc: SessionContinuityEndpoints[Session] = oneOffSession
62+
override lazy val sc: TapirSessionContinuity[Session] = oneOffSession
6363
override lazy val st: SetSessionTransport = HeaderST
6464
override lazy val checkMode: TapirCsrfCheckMode[Session] = checkHeaderAndForm
6565
}
6666

6767
case class RefreshableCookieSessionEndpoints(system: ActorSystem[_], checkHeaderAndForm: Boolean)
6868
extends SessionEndpoints {
69-
override lazy val sc: SessionContinuityEndpoints[Session] = refreshableSession
69+
override lazy val sc: TapirSessionContinuity[Session] = refreshableSession
7070
override lazy val st: SetSessionTransport = CookieST
7171
override lazy val checkMode: TapirCsrfCheckMode[Session] = checkHeaderAndForm
7272
}
7373

7474
case class RefreshableHeaderSessionEndpoints(system: ActorSystem[_], checkHeaderAndForm: Boolean)
7575
extends SessionEndpoints {
76-
override lazy val sc: SessionContinuityEndpoints[Session] = refreshableSession
76+
override lazy val sc: TapirSessionContinuity[Session] = refreshableSession
7777
override lazy val st: SetSessionTransport = HeaderST
7878
override lazy val checkMode: TapirCsrfCheckMode[Session] = checkHeaderAndForm
7979
}
Lines changed: 60 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,33 @@
11
package com.softwaremill.session
22

3-
import sttp.model.Method._
3+
import sttp.model.Method
44
import sttp.model.headers.{CookieValueWithMeta, CookieWithMeta}
5-
import sttp.model.{Method, StatusCode}
6-
import sttp.monad.FutureMonad
7-
import sttp.tapir.server.PartialServerEndpointWithSecurityOutput
85
import sttp.tapir._
6+
import sttp.tapir.server.PartialServerEndpointWithSecurityOutput
97

108
import scala.concurrent.{ExecutionContext, Future}
119

12-
private[session] trait CsrfEndpoints[T] extends CsrfCheck {
13-
14-
import com.softwaremill.session.AkkaToTapirImplicits._
15-
16-
def manager: SessionManager[T]
17-
18-
implicit def ec: ExecutionContext
19-
20-
def csrfCookie: EndpointIO.Header[Option[CookieValueWithMeta]] =
21-
setCookieOpt(manager.config.csrfCookieConfig.name).description("set csrf token as cookie")
22-
23-
def submittedCsrfCookie: EndpointInput.Cookie[Option[String]] =
24-
cookie(manager.config.csrfCookieConfig.name)
25-
26-
def submittedCsrfHeader: EndpointIO.Header[Option[String]] = header[Option[String]](
27-
manager.config.csrfSubmittedName
28-
).description("read csrf token as header")
29-
30-
def setNewCsrfToken(): CookieWithMeta = manager.csrfManager.createCookie()
31-
32-
def hmacTokenCsrfProtectionLogic(
33-
method: Method,
34-
csrfTokenFromCookie: Option[String],
35-
submittedCsrfToken: Option[String]
36-
): Either[Unit, (Option[CookieValueWithMeta], Unit)] = {
37-
csrfTokenFromCookie match {
38-
case Some(cookie) =>
39-
val token = cookie
40-
submittedCsrfToken match {
41-
case Some(submitted) =>
42-
if (submitted == token && token.nonEmpty && manager.csrfManager.validateToken(token)) {
43-
Right((None, ()))
44-
} else {
45-
Left(())
46-
}
47-
case _ =>
48-
Left(())
49-
}
50-
// if a cookie is not set, generating a new one for **get** requests, rejecting other
51-
case _ =>
52-
if (method.is(GET)) {
53-
Right((Some(manager.csrfManager.createCookie().valueWithMeta), ()))
54-
} else {
55-
Left(())
56-
}
57-
}
58-
}
59-
60-
def hmacTokenCsrfProtection[
61-
SECURITY_INPUT,
62-
PRINCIPAL,
63-
SECURITY_OUTPUT
64-
](
10+
trait CsrfEndpoints {
11+
12+
/** Protects against CSRF attacks using a double-submit cookie. The cookie will be set on any
13+
* `GET` request which doesn't have the token set in the header. For all other requests, the
14+
* value of the token from the CSRF cookie must match the value in the custom header (or request
15+
* body, if `checkFormBody` is `true`).
16+
*
17+
* The cookie value is the concatenation of a timestamp and its HMAC hash following the OWASP
18+
* recommendation for CSRF prevention:
19+
* @see
20+
* <a
21+
* href="https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#hmac-based-token-pattern">OWASP</a>
22+
*
23+
* Note that this scheme can be broken when not all subdomains are protected or not using HTTPS
24+
* and secure cookies, and the token is placed in the request body (not in the header).
25+
*
26+
* See the documentation for more details.
27+
*/
28+
def hmacTokenCsrfProtection[T, SECURITY_INPUT, PRINCIPAL, SECURITY_OUTPUT](
29+
checkMode: TapirCsrfCheckMode[T]
30+
)(
6531
body: => PartialServerEndpointWithSecurityOutput[
6632
SECURITY_INPUT,
6733
PRINCIPAL,
@@ -81,86 +47,48 @@ private[session] trait CsrfEndpoints[T] extends CsrfCheck {
8147
Unit,
8248
Any,
8349
Future
84-
] = {
85-
val partial =
86-
// extract csrf token from cookie
87-
csrfTokenFromCookie()
88-
// extract request method
89-
.securityIn(extractFromRequest(req => req.method))
90-
// extract submitted csrf token from header
91-
.securityIn(submittedCsrfHeader)
92-
// extract submitted csrf token from form
93-
.securityIn(formBody[Map[String, String]])
94-
.out(csrfCookie)
95-
.errorOut(statusCode(StatusCode.Unauthorized))
96-
.serverSecurityLogicWithOutput {
97-
case (
98-
csrfTokenFromCookie,
99-
method,
100-
submittedCsrfTokenFromHeader,
101-
submittedCsrfTokenFromForm
102-
) =>
103-
Future.successful(
104-
hmacTokenCsrfProtectionLogic(
105-
method,
106-
csrfTokenFromCookie,
107-
if (checkHeaderAndForm)
108-
submittedCsrfTokenFromHeader.fold(
109-
submittedCsrfTokenFromForm.get(manager.config.csrfSubmittedName)
110-
)(Option(_))
111-
else
112-
submittedCsrfTokenFromHeader
113-
)
114-
)
115-
}
116-
partial.endpoint
117-
.prependSecurityIn(body.securityInput)
118-
.out(body.securityOutput)
119-
.out(partial.securityOutput)
120-
.serverSecurityLogicWithOutput {
121-
case (
122-
securityInput,
123-
csrfTokenFromCookie,
124-
method,
125-
submittedCsrfTokenFromHeader,
126-
submittedCsrfTokenFromForm
127-
) =>
128-
partial
129-
.securityLogic(new FutureMonad())(
130-
(
131-
csrfTokenFromCookie,
132-
method,
133-
submittedCsrfTokenFromHeader,
134-
submittedCsrfTokenFromForm
135-
)
136-
)
137-
.flatMap {
138-
case Left(l) => Future.successful(Left(l))
139-
case Right(r) =>
140-
body.securityLogic(new FutureMonad())(securityInput).map {
141-
case Left(l2) => Left(l2)
142-
case Right(r2) =>
143-
Right(((r2._1, r._1), r2._2))
144-
}
145-
}
146-
}
147-
}
50+
] =
51+
checkMode.hmacTokenCsrfProtection {
52+
body
53+
}
14854

149-
def csrfTokenFromCookie(): Endpoint[Option[String], Unit, Unit, Unit, Any] =
150-
endpoint
151-
// extract csrf token from cookie
152-
.securityIn(submittedCsrfCookie)
55+
def csrfCookie[T](
56+
checkMode: TapirCsrfCheckMode[T]
57+
): EndpointIO.Header[Option[CookieValueWithMeta]] =
58+
checkMode.csrfCookie
15359

154-
}
60+
def csrfTokenFromCookie[T](
61+
checkMode: TapirCsrfCheckMode[T]
62+
): Endpoint[Option[String], Unit, Unit, Unit, Any] =
63+
checkMode.csrfTokenFromCookie()
15564

156-
sealed trait CsrfCheck {
157-
def checkHeaderAndForm: Boolean
65+
def setNewCsrfToken[T](
66+
checkMode: TapirCsrfCheckMode[T]
67+
): CookieWithMeta =
68+
checkMode.setNewCsrfToken()
15869
}
15970

160-
trait CsrfCheckHeader extends CsrfCheck {
161-
val checkHeaderAndForm: Boolean = false
71+
object CsrfEndpoints extends CsrfEndpoints
72+
73+
object TapirCsrfOptions {
74+
def checkHeader[T](implicit manager: SessionManager[T], ec: ExecutionContext) =
75+
new TapirCsrfCheckHeader[T]()
76+
def checkHeaderAndForm[T](implicit manager: SessionManager[T], ec: ExecutionContext) =
77+
new TapirCsrfCheckHeaderAndForm[T]()
16278
}
16379

164-
trait CsrfCheckHeaderAndForm extends CsrfCheck {
165-
val checkHeaderAndForm: Boolean = true
80+
sealed trait TapirCsrfCheckMode[T] extends TapirCsrf[T] {
81+
def manager: SessionManager[T]
82+
def ec: ExecutionContext
83+
def csrfManager: CsrfManager[T] = manager.csrfManager
16684
}
85+
86+
class TapirCsrfCheckHeader[T](implicit val manager: SessionManager[T], val ec: ExecutionContext)
87+
extends TapirCsrfCheckMode[T]
88+
with CsrfCheckHeader
89+
90+
class TapirCsrfCheckHeaderAndForm[T](implicit
91+
val manager: SessionManager[T],
92+
val ec: ExecutionContext
93+
) extends TapirCsrfCheckMode[T]
94+
with CsrfCheckHeaderAndForm

session/core/src/main/scala/com/softwaremill/session/OneOffSessionEndpoints.scala renamed to session/core/src/main/scala/com/softwaremill/session/OneOffTapirSession.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import sttp.tapir.{cookie, header, _}
99

1010
import scala.concurrent.{ExecutionContext, Future}
1111

12-
private[session] trait OneOffSessionEndpoints[T] {
13-
import AkkaToTapirImplicits._
12+
private[session] trait OneOffTapirSession[T] {
13+
import TapirImplicits._
1414

1515
implicit def manager: SessionManager[T]
1616

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import scala.concurrent.{ExecutionContext, Future}
1212
import scala.language.implicitConversions
1313
import scala.util.{Failure, Success}
1414

15-
private[session] trait RefreshableSessionEndpoints[T] extends Completion {
16-
this: OneOffSessionEndpoints[T] =>
17-
import com.softwaremill.session.AkkaToTapirImplicits._
15+
private[session] trait RefreshableTapirSession[T] extends Completion {
16+
this: OneOffTapirSession[T] =>
17+
import com.softwaremill.session.TapirImplicits._
1818

1919
implicit def refreshTokenStorage: RefreshTokenStorage[T]
2020

session/core/src/main/scala/com/softwaremill/session/TapirSession.scala renamed to session/core/src/main/scala/com/softwaremill/session/SessionEndpoints.scala

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import sttp.tapir.server.PartialServerEndpointWithSecurityOutput
55

66
import scala.concurrent.{ExecutionContext, Future}
77

8-
trait TapirSession {
8+
trait SessionEndpoints {
99

1010
/** Set the session cookie with the session content. The content is signed, optionally encrypted
1111
* and with an optional expiry date.
@@ -14,7 +14,7 @@ trait TapirSession {
1414
* cookie.
1515
*/
1616
def setSession[T, INPUT](
17-
sc: SessionContinuityEndpoints[T],
17+
sc: TapirSessionContinuity[T],
1818
st: SetSessionTransport
1919
)(endpoint: => Endpoint[INPUT, Unit, Unit, Unit, Any])(implicit
2020
f: INPUT => Option[T]
@@ -29,7 +29,7 @@ trait TapirSession {
2929
* If refreshable, tries to create a new session based on the refresh token cookie.
3030
*/
3131
def session[T](
32-
sc: SessionContinuityEndpoints[T],
32+
sc: TapirSessionContinuity[T],
3333
st: GetSessionTransport,
3434
required: Option[Boolean]
3535
): PartialServerEndpointWithSecurityOutput[Seq[Option[String]], SessionResult[T], Unit, Unit, Seq[
@@ -46,7 +46,7 @@ trait TapirSession {
4646
* users.
4747
*/
4848
def invalidateSession[T, SECURITY_INPUT, PRINCIPAL](
49-
sc: SessionContinuityEndpoints[T]
49+
sc: TapirSessionContinuity[T]
5050
)(
5151
body: => PartialServerEndpointWithSecurityOutput[
5252
SECURITY_INPUT,
@@ -73,7 +73,7 @@ trait TapirSession {
7373
/** Read an optional session from the session cookie.
7474
*/
7575
def optionalSession[T](
76-
sc: SessionContinuityEndpoints[T],
76+
sc: TapirSessionContinuity[T],
7777
st: GetSessionTransport
7878
): PartialServerEndpointWithSecurityOutput[Seq[Option[String]], Option[T], Unit, Unit, Seq[
7979
Option[String]
@@ -83,15 +83,15 @@ trait TapirSession {
8383
/** Read a required session from the session cookie.
8484
*/
8585
def requiredSession[T](
86-
sc: SessionContinuityEndpoints[T],
86+
sc: TapirSessionContinuity[T],
8787
st: GetSessionTransport
8888
): PartialServerEndpointWithSecurityOutput[Seq[Option[String]], T, Unit, Unit, Seq[
8989
Option[String]
9090
], Unit, Any, Future] =
9191
sc.requiredSession(st)
9292

9393
def touchSession[T](
94-
sc: SessionContinuityEndpoints[T],
94+
sc: TapirSessionContinuity[T],
9595
st: GetSessionTransport,
9696
required: Option[Boolean]
9797
): PartialServerEndpointWithSecurityOutput[Seq[Option[String]], SessionResult[T], Unit, Unit, Seq[
@@ -103,7 +103,7 @@ trait TapirSession {
103103
* [[SessionConfig.sessionMaxAgeSeconds]] option, as it sets the expiry date anew.
104104
*/
105105
def touchOptionalSession[T](
106-
sc: SessionContinuityEndpoints[T],
106+
sc: TapirSessionContinuity[T],
107107
st: GetSessionTransport
108108
): PartialServerEndpointWithSecurityOutput[Seq[Option[String]], Option[T], Unit, Unit, Seq[
109109
Option[String]
@@ -115,7 +115,7 @@ trait TapirSession {
115115
* [[SessionConfig.sessionMaxAgeSeconds]] option, as it sets the expiry date anew.
116116
*/
117117
def touchRequiredSession[T](
118-
sc: SessionContinuityEndpoints[T],
118+
sc: TapirSessionContinuity[T],
119119
st: GetSessionTransport
120120
): PartialServerEndpointWithSecurityOutput[Seq[Option[String]], T, Unit, Unit, Seq[
121121
Option[String]
@@ -137,15 +137,15 @@ object TapirSessionOptions {
137137
}
138138

139139
class OneOffTapir[T](implicit val manager: SessionManager[T], val ec: ExecutionContext)
140-
extends OneOffSessionContinuity[T]
141-
with OneOffSessionEndpoints[T]
140+
extends OneOffTapirSessionContinuity[T]
141+
with OneOffTapirSession[T]
142142

143143
class RefreshableTapir[T](implicit
144144
val manager: SessionManager[T],
145145
val refreshTokenStorage: RefreshTokenStorage[T],
146146
val ec: ExecutionContext
147-
) extends RefreshableSessionContinuity[T]
148-
with RefreshableSessionEndpoints[T]
149-
with OneOffSessionEndpoints[T]
147+
) extends RefreshableTapirSessionContinuity[T]
148+
with RefreshableTapirSession[T]
149+
with OneOffTapirSession[T]
150150

151-
object TapirSession extends TapirSession {}
151+
object SessionEndpoints extends SessionEndpoints {}

0 commit comments

Comments
 (0)