Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#54 refactor better exceptions handling and cleaner code in sigin end…
Browse files Browse the repository at this point in the history
…point
dniel committed Mar 16, 2019

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 2abe747 commit 7fa9b77
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
@@ -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.