Skip to content

Commit

Permalink
#151 add login and logout endpoint for clients to call for user when…
Browse files Browse the repository at this point in the history
… handling user login and logout.
  • Loading branch information
dniel committed Jun 29, 2020
1 parent e1d484f commit 9746286
Show file tree
Hide file tree
Showing 16 changed files with 352 additions and 100 deletions.
3 changes: 2 additions & 1 deletion src/main/kotlin/dniel/forwardauth/AuthProperties.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ class AuthProperties {

val application = apps.find() { it.name.equals(name, ignoreCase = true) }
if (application !== null) {
application.returnTo = if (application.returnTo.isNotEmpty()) application.returnTo else default.returnTo
application.logoutUri = if (application.logoutUri.isNotEmpty()) application.logoutUri else default.logoutUri
application.loginUri = if (application.loginUri.isNotEmpty()) application.loginUri else default.loginUri
application.redirectUri = if (application.redirectUri.isNotEmpty()) application.redirectUri else default.redirectUri
application.audience = if (application.audience.isNotEmpty()) application.audience else default.audience
application.scope = if (application.scope.isNotEmpty()) application.scope else default.scope
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import java.net.URI
*
*/
@Component
class SigninHandler(val properties: AuthProperties,
val auth0Client: Auth0Client) : CommandHandler<SigninHandler.SigninCommand> {
class CallbackHandler(val properties: AuthProperties,
val auth0Client: Auth0Client) : CommandHandler<CallbackHandler.SigninCommand> {

private val LOGGER = LoggerFactory.getLogger(this::class.java)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package dniel.forwardauth.application.commandhandlers

import dniel.forwardauth.AuthProperties
import dniel.forwardauth.application.Command
import dniel.forwardauth.application.CommandHandler
import dniel.forwardauth.domain.authorize.AuthorizeNonce
import dniel.forwardauth.domain.authorize.AuthorizeState
import dniel.forwardauth.domain.authorize.AuthorizeUrl
import dniel.forwardauth.domain.authorize.RequestedUrl
import dniel.forwardauth.domain.events.Event
import dniel.forwardauth.domain.shared.Application
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.net.URI

/**
* Handle Logout of user.
*
*/
@Component
class LoginHandler(private val properties: AuthProperties) : CommandHandler<LoginHandler.LoginCommand> {

private val LOGGER = LoggerFactory.getLogger(this::class.java)

/**
* This is the input parameter object for the handler to pass inn all
* needed parameters to the handler.
* @param forwardedHost is the name of the application used to signout.
*/
data class LoginCommand(val forwardedHost: String) : Command


/**
* This command can produce a set of events as response from the handle method.
*/
sealed class LoginEvent(val app: Application) : Event() {
class LoginRedirect(val redirectUrl: URI,
val nonce: AuthorizeNonce,
val tokenCookieDomain: String,
val maxNonceAge: Int,
app: Application) : LoginEvent(app)

class Error(val reason: String = "Unknown error", app: Application) : LoginEvent(app)
}

/**
* Main handle Sign out method.
* <p/>
* @return an sign out event containing the result status of the sign out.
*/
override fun handle(params: LoginCommand): Event {
LOGGER.debug("Login with Auth0")
val app = properties.findApplicationOrDefault(params.forwardedHost)

// just abort if no login url is set, nowhere to redirect user after login.
if(app.loginUri.isNullOrBlank()){
return LoginEvent.Error("Missing login url in configuration.", app)
}

val authUrl = properties.authorizeUrl
val nonce = AuthorizeNonce.generate()
val loginUrl = URI.create(app.loginUri)
val originUrl = RequestedUrl(loginUrl.scheme, loginUrl.host, loginUrl.path, "GET")
val state = AuthorizeState.create(originUrl, nonce)
val authorizeUrl = AuthorizeUrl(authUrl, app, state)
val tokenCookieDomain = app.tokenCookieDomain
val maxNonceAge = properties.nonceMaxAge

try {
return LoginEvent.LoginRedirect(authorizeUrl.toURI(), nonce, tokenCookieDomain, maxNonceAge, app)
} catch (e: Exception) {
return LoginEvent.Error(e.message!!, app)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component

/**
* Handle Sign out of user.
* Handle Logout of user.
*
*/
@Component
class SignoutHandler(val properties: AuthProperties,
val auth0Client: Auth0Client) : CommandHandler<SignoutHandler.SignoutCommand> {
class LogoutHandler(val properties: AuthProperties,
val auth0Client: Auth0Client) : CommandHandler<LogoutHandler.LogoutCommand> {

private val LOGGER = LoggerFactory.getLogger(this::class.java)

Expand All @@ -25,38 +25,38 @@ class SignoutHandler(val properties: AuthProperties,
* @param accessToken is the user token to use for the signout request to the IDP
* @param forwardedHost is the name of the host used to signout.
*/
data class SignoutCommand(val forwardedHost: String,
val accessToken: String) : Command
data class LogoutCommand(val forwardedHost: String,
val accessToken: String) : Command


/**
* This command can produce a set of events as response from the handle method.
*/
sealed class SignoutEvent(val app: Application) : Event() {
class SignoutComplete(app: Application) : SignoutEvent(app)
class SignoutRedirect(val redirectUrl: String, app: Application) : SignoutEvent(app)
class Error(val reason: String = "Unknown error", app: Application) : SignoutEvent(app)
sealed class LogoutEvent(val app: Application) : Event() {
class LogoutComplete(app: Application) : LogoutEvent(app)
class LogoutRedirect(val redirectUrl: String, app: Application) : LogoutEvent(app)
class Error(val reason: String = "Unknown error", app: Application) : LogoutEvent(app)
}

/**
* Main handle Sign out method.
* <p/>
* @return an sign out event containing the result status of the sign out.
*/
override fun handle(params: SignoutCommand): Event {
override fun handle(params: LogoutCommand): Event {
LOGGER.debug("Sign out from Auth0")
val app = properties.findApplicationOrDefault(params.forwardedHost)
try {
val signout = auth0Client.signout(app.clientId, app.returnTo)
if (!signout.isNullOrEmpty()) {
LOGGER.debug("Signout done, redirect to ${signout}")
return SignoutEvent.SignoutRedirect(signout, app)
val logout = auth0Client.logout(app.clientId, app.logoutUri)
if (!logout.isNullOrEmpty()) {
LOGGER.debug("Logout done, redirect to ${logout}")
return LogoutEvent.LogoutRedirect(logout, app)
} else {
LOGGER.debug("Signout done.")
return SignoutEvent.SignoutComplete(app)
LOGGER.debug("Logout done.")
return LogoutEvent.LogoutComplete(app)
}
} catch (e: Exception) {
return SignoutEvent.Error(e.message!!, app)
return LogoutEvent.Error(e.message!!, app)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ class Application {
var clientSecret: String = ""
var audience: String = ""
var scope: String = "profile openid email"
var redirectUri: String = ""
var tokenCookieDomain: String = ""
var restrictedMethods: Array<String> = arrayOf("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT")
var requiredPermissions: Array<String> = emptyArray()
var claims: Array<String> = emptyArray()
var returnTo: String = ""
var redirectUri: String = "" // the callback uri to return with authorization code to.
var logoutUri: String = "" // the logout url to return to after logout with /logout endpoint.
var loginUri: String = "" // the login url to return to after login with /login endpoint.

override fun toString(): String {
return "Application(name='$name', clientId='$clientId', clientSecret='$clientSecret', audience='$audience', scope='$scope', redirectUri='$redirectUri', tokenCookieDomain='$tokenCookieDomain', restrictedMethods=${Arrays.toString(restrictedMethods)}, requiredPermissions=${Arrays.toString(requiredPermissions)}, claims=${Arrays.toString(claims)})"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ class Auth0Client(val properties: AuthProperties) {
* @param returnTo the url to redirect to after the signout has been completed.
* @return url to redirect to if one is requested, or empty if no redirect returned.
*/
fun signout(clientId: String, returnTo: String): String? {
fun logout(clientId: String, returnTo: String): String? {
LOGGER.debug("Perform Sign Out")
Unirest.setHttpClient(org.apache.http.impl.client.HttpClients.custom()
.disableRedirectHandling()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,20 @@ class SecurityConfiguration : WebSecurityConfigurerAdapter() {
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.csrf().disable()
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.httpBasic().disable();
http.logout().disable()
http.authorizeRequests()//
.antMatchers("/v3/api-docs/").permitAll()
.antMatchers("/authorize").permitAll()
.antMatchers("/signin").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/actuator/info").permitAll()
.antMatchers("/actuator/health").permitAll()
.antMatchers("/events").hasAuthority("admin:forwardauth")
.antMatchers("/ui/**").hasAuthority("admin:forwardauth")
.anyRequest().authenticated();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter::class.java)
http.addFilterBefore(basicAuthFilter, AuthenticationTokenFilter::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ package dniel.forwardauth.infrastructure.spring.controllers

import dniel.forwardauth.AuthProperties
import dniel.forwardauth.application.CommandDispatcher
import dniel.forwardauth.application.commandhandlers.SigninHandler
import dniel.forwardauth.infrastructure.auth0.Auth0Client
import dniel.forwardauth.application.commandhandlers.CallbackHandler
import dniel.forwardauth.infrastructure.spring.exceptions.ApplicationException
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
Expand All @@ -16,10 +15,9 @@ import javax.servlet.http.HttpServletResponse
* Callback Endpoint for Auth0 signin to retrieve JWT token from code.
*/
@RestController
class SigninController(val properties: AuthProperties,
val auth0Client: Auth0Client,
val signinHandler: SigninHandler,
val commandDispatcher: CommandDispatcher) : BaseController() {
class CallbackController(val properties: AuthProperties,
val callbackHandler: CallbackHandler,
val commandDispatcher: CommandDispatcher) : BaseController() {
private val LOGGER = LoggerFactory.getLogger(this.javaClass)

/**
Expand All @@ -45,18 +43,18 @@ class SigninController(val properties: AuthProperties,
@CookieValue("AUTH_NONCE") nonce: String?,
response: HttpServletResponse): ResponseEntity<String> {
printHeaders(headers)
val command: SigninHandler.SigninCommand = SigninHandler.SigninCommand(forwardedHost, code, error, errorDescription, state, nonce)
val signinEvent = commandDispatcher.dispatch(signinHandler, command) as SigninHandler.SigninEvent
val command: CallbackHandler.SigninCommand = CallbackHandler.SigninCommand(forwardedHost, code, error, errorDescription, state, nonce)
val signinEvent = commandDispatcher.dispatch(callbackHandler, command) as CallbackHandler.SigninEvent

return when (signinEvent) {
is SigninHandler.SigninEvent.SigninComplete -> {
is CallbackHandler.SigninEvent.SigninComplete -> {
addCookie(response, "ACCESS_TOKEN", signinEvent.accessToken, signinEvent.app.tokenCookieDomain, signinEvent.expiresIn)
addCookie(response, "JWT_TOKEN", signinEvent.idToken, signinEvent.app.tokenCookieDomain, signinEvent.expiresIn)
clearCookie(response, "AUTH_NONCE", signinEvent.app.tokenCookieDomain)
LOGGER.info("SignInSuccessful, redirect to '${signinEvent.redirectTo}'")
ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT).location(signinEvent.redirectTo).build()
}
is SigninHandler.SigninEvent.Error -> throw ApplicationException(signinEvent.reason)
is CallbackHandler.SigninEvent.Error -> throw ApplicationException(signinEvent.reason)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package dniel.forwardauth.infrastructure.spring.controllers

import dniel.forwardauth.infrastructure.spring.exceptions.PermissionDeniedException
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.web.ServerProperties
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver
import org.springframework.boot.web.servlet.error.ErrorAttributes
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Component
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.ModelAndView
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse


//@Component
class ErrorController(errorAttributes: ErrorAttributes?,
serverProperties: ServerProperties,
errorViewResolvers: List<ErrorViewResolver?>?) : BasicErrorController(errorAttributes, serverProperties.error, errorViewResolvers) {

override fun errorHtml(request: HttpServletRequest?, response: HttpServletResponse?): ModelAndView {
response!!.setHeader("testHtml", "test")
return super.errorHtml(request, response)
}

override fun error(request: HttpServletRequest): ResponseEntity<Map<String, Any>> {
val body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL))
val status = getStatus(request)
val headers = HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON_UTF8)
headers.put("test", listOf("test"))
return ResponseEntity(body, headers, status)
}

companion object {
private val LOGGER = LoggerFactory.getLogger(this.javaClass)
}

init {
LOGGER.info("Created")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.net.URI

@RestController()
@RestController
internal class EventController(val properties: AuthProperties, val repo: EventRepository) {

private val LOGGER = LoggerFactory.getLogger(this.javaClass)
Expand Down
Loading

0 comments on commit 9746286

Please sign in to comment.