Skip to content

Commit

Permalink
#54 refactor better exceptions handling and cleaner code in sigin end…
Browse files Browse the repository at this point in the history
…point
  • Loading branch information
dniel committed Mar 16, 2019
1 parent 2abe747 commit 7fa9b77
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 49 deletions.

This file was deleted.

7 changes: 6 additions & 1 deletion src/main/kotlin/dniel/forwardauth/domain/SigninException.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package dniel.forwardauth.domain

import javax.ws.rs.WebApplicationException
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response

class SigninException : WebApplicationException {
constructor(error: String, description: String) : super("${error}: ${description}", Response.Status.BAD_REQUEST)
constructor(error: String, description: String? = "no message") : super(
Response.status(Response.Status.BAD_REQUEST)
.entity("${error}: ${description}")
.type(MediaType.APPLICATION_JSON)
.build())
}
13 changes: 13 additions & 0 deletions src/main/kotlin/dniel/forwardauth/domain/UnauthorizedException.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dniel.forwardauth.domain

import javax.ws.rs.WebApplicationException
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response

class UnauthorizedException : WebApplicationException {
constructor(error: String, description: String? = "unknown") : super(
Response.status(Response.Status.FORBIDDEN)
.entity("${error}: ${description}")
.type(MediaType.APPLICATION_JSON)
.build())
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package dniel.forwardauth.infrastructure.endpoints

import dniel.forwardauth.AuthProperties
import dniel.forwardauth.domain.SigninException
import dniel.forwardauth.domain.State
import dniel.forwardauth.domain.UnauthorizedException
import dniel.forwardauth.domain.service.TokenService
import dniel.forwardauth.infrastructure.auth0.Auth0Service
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -33,56 +35,67 @@ class SigninEndpoint(val properties: AuthProperties, val auth0Client: Auth0Servi
@QueryParam("state") state: String,
@HeaderParam("x-forwarded-host") forwardedHost: String,
@CookieParam("AUTH_NONCE") nonceCookie: Cookie): Response {
printHeaders(headers)

if (error != null && error == "unauthorized") {
return Response.status(Response.Status.FORBIDDEN).entity(errorDescription).build()
} else if (error != null) {
throw WebApplicationException(errorDescription)
} else if (code != null) {
LOGGER.debug("SignIn with code=$code")
val app = properties.findApplicationOrDefault(forwardedHost)
val audience = app.audience
val tokenCookieDomain = app.tokenCookieDomain
if (LOGGER.isTraceEnabled) {
printHeaders(headers)
}
return when {
error.isNullOrEmpty() && code.isNullOrEmpty() -> throw SigninException("Missing field. One of the fields 'code' or 'error' must be filled out.")
error == "unauthorized" -> throw UnauthorizedException(error, errorDescription)
!error.isNullOrEmpty() -> throw SigninException(error, errorDescription)
!code.isNullOrEmpty() -> performSignin(code, forwardedHost, state, nonceCookie)
else -> throw SigninException("Illegal State", "Shouldn't be possible to end in this state.")
}
}

// TODO move into NonceService and add proper errorhandling if nnonce check fails.
val decodedState = State.decode(state)
val receivedNonce = decodedState.nonce.value
val sentNonce = nonceCookie.value
if (receivedNonce != sentNonce) {
LOGGER.error("SignInFailedNonce received=$receivedNonce sent=$sentNonce")
}
/**
* Verify Signin and set user session cookies.
* @param code is the exchange code for tokens.
* @param forwardedHost is the host header set by Traefik when it forwards a requests
* @param nonceCookie the secret nonce to prevent CORS set in browser cookie.
* @param state Is state sent whil authenticating and should contain the same nonce as the noonce cookie + some more.
*
* TODO should extract this method into an application service like I have aready done with AuthorizationCommandHandler
* so that its easier to write unit tests, separating the code from http/rest technical code into pure application logic.
*/
private fun performSignin(code: String, forwardedHost: String, state: String, nonceCookie: Cookie): Response {
LOGGER.debug("SignIn with code=$code")
val app = properties.findApplicationOrDefault(forwardedHost)
val audience = app.audience
val tokenCookieDomain = app.tokenCookieDomain

val response = auth0Client.authorizationCodeExchange(code, app.clientId, app.clientSecret, app.redirectUri)
val accessToken = response.get("access_token") as String
val idToken = response.get("id_token") as String
// TODO move into NonceService and add proper errorhandling if nnonce check fails.
val decodedState = State.decode(state)
val receivedNonce = decodedState.nonce.value
val sentNonce = nonceCookie.value
if (receivedNonce != sentNonce) {
LOGGER.error("SignInFailedNonce received=$receivedNonce sent=$sentNonce")
}

if (shouldVerifyAccessToken(app)) {
verifyTokenService.verify(accessToken, audience, DOMAIN)
}
val accessTokenCookie = NewCookie("ACCESS_TOKEN", accessToken, "/", tokenCookieDomain, null, -1, false, true)
val jwtCookie = NewCookie("JWT_TOKEN", idToken, "/", tokenCookieDomain, null, -1, false, true)
val nonceCookie = NewCookie("AUTH_NONCE", "deleted", "/", tokenCookieDomain, null, 0, false, true)
val response = auth0Client.authorizationCodeExchange(code, app.clientId, app.clientSecret, app.redirectUri)
val accessToken = response.get("access_token") as String
val idToken = response.get("id_token") as String

LOGGER.info("SignInSuccessful, redirect to originUrl originUrl=${decodedState.originUrl}")
return Response
.temporaryRedirect(decodedState.originUrl.uri())
.cookie(jwtCookie)
.cookie(accessTokenCookie)
.cookie(nonceCookie)
.build()
} else {
throw WebApplicationException("Missing field. One of the fields 'code' or 'error' must be filled out.")
if (shouldVerifyAccessToken(app)) {
verifyTokenService.verify(accessToken, audience, DOMAIN)
}
val accessTokenCookie = NewCookie("ACCESS_TOKEN", accessToken, "/", tokenCookieDomain, null, -1, false, true)
val jwtCookie = NewCookie("JWT_TOKEN", idToken, "/", tokenCookieDomain, null, -1, false, true)
val nonceCookie = NewCookie("AUTH_NONCE", "deleted", "/", tokenCookieDomain, null, 0, false, true)

LOGGER.info("SignInSuccessful, redirect to originUrl originUrl=${decodedState.originUrl}")
return Response
.temporaryRedirect(decodedState.originUrl.uri())
.cookie(jwtCookie)
.cookie(accessTokenCookie)
.cookie(nonceCookie)
.build()
}

private fun shouldVerifyAccessToken(app: AuthProperties.Application): Boolean = !app.audience.equals("${DOMAIN}userinfo")

private fun printHeaders(headers: HttpHeaders) {
if (LOGGER.isTraceEnabled) {
for (requestHeader in headers.requestHeaders) {
LOGGER.trace("Header ${requestHeader.key} = ${requestHeader.value}")
}
for (requestHeader in headers.requestHeaders) {
LOGGER.trace("Header ${requestHeader.key} = ${requestHeader.value}")
}
}
}

0 comments on commit 7fa9b77

Please sign in to comment.