Skip to content

Commit

Permalink
feat(web): introduction of an access denied page
Browse files Browse the repository at this point in the history
When a user does not have the required permissions, a redirection url to a new 'Access Denied' page is passed back to the reverse proxy along with the 403 error code. This follows the same sequence as an unauthenticated user getting redirected to the login. The Access Denied page presents the user with an option to logout, as well as the details of the page they were denied access to.

Work includes a slight refactor of authz handlers to allow for code reuse.

Also includes a simple 'demo home' button injected into the development login page to allow for easier navigation back to home.example.com for the Standalone suite.

Fixes authelia#2319.

Signed-off-by: Rocket Proto <[email protected]>
  • Loading branch information
rocketproto committed Apr 28, 2024
1 parent 6dcf481 commit bd9e477
Show file tree
Hide file tree
Showing 24 changed files with 507 additions and 108 deletions.
11 changes: 10 additions & 1 deletion internal/handlers/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
queryArgConsentID = "consent_id"
queryArgWorkflow = "workflow"
queryArgWorkflowID = "workflow_id"
queryArgEC = "ec"
)

var (
Expand All @@ -54,6 +55,14 @@ var (
qryArgConsentID = []byte(queryArgConsentID)
)

var (
baseErrorPath = "error"
)

var (
errorForbidden = "forbidden"
)

var (
qryValueBasic = []byte("basic")
qryValueEmpty = []byte("")
Expand Down Expand Up @@ -91,7 +100,7 @@ const (
)

const (
logFmtAuthzRedirect = "Access to %s (method %s) is not authorized to user %s, responding with status code %d with location redirect to %s"
logFmtAuthzRedirect = "Access to %s (method %s) is not granted to user %s, responding with status code %d with location redirect to %s"

logFmtAuthorizationPrefix = "Authorization Request with id '%s' on client with id '%s' "

Expand Down
37 changes: 32 additions & 5 deletions internal/handlers/handler_authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@ func (authz *Authz) Handler(ctx *middlewares.AutheliaCtx) {

switch isAuthzResult(authn.Level, required, ruleHasSubject) {
case AuthzResultForbidden:
ctx.Logger.Infof("Access to '%s' is forbidden to user '%s'", object.URL.String(), authn.Username)
ctx.ReplyForbidden()
authz.handleForbidden(ctx, authn, authz.getErrorRedirectionURL(&object, autheliaURL, errorForbidden))
case AuthzResultUnauthorized:
var handler HandlerAuthzUnauthorized

Expand Down Expand Up @@ -134,12 +133,12 @@ func (authz *Authz) getAutheliaURL(ctx *middlewares.AutheliaCtx, provider *sessi
return nil, fmt.Errorf("authelia url lookup failed")
}

func (authz *Authz) getRedirectionURL(object *authorization.Object, autheliaURL *url.URL) (redirectionURL *url.URL) {
if autheliaURL == nil {
func (authz *Authz) getRedirectionURL(object *authorization.Object, baseURL *url.URL) (redirectionURL *url.URL) {
if baseURL == nil {
return nil
}

redirectionURL, _ = url.ParseRequestURI(autheliaURL.String())
redirectionURL, _ = url.ParseRequestURI(baseURL.String())

if redirectionURL.Path == "" {
redirectionURL.Path = "/"
Expand All @@ -158,6 +157,34 @@ func (authz *Authz) getRedirectionURL(object *authorization.Object, autheliaURL
return redirectionURL
}

func (authz *Authz) getErrorRedirectionURL(object *authorization.Object, baseURL *url.URL, errorString string) (errorRedirectionURL *url.URL) {
if baseURL == nil {
return nil
}

baseErrorURL, _ := url.ParseRequestURI(baseURL.String())

if baseErrorURL.Path == "" {
baseErrorURL.Path = "/"
}

baseErrorURL.Path += baseErrorPath

qry := baseErrorURL.Query()

if object.Method != "" {
qry.Set(queryArgRM, object.Method)
}

qry.Set(queryArgEC, errorString)

baseErrorURL.RawQuery = qry.Encode()

errorRedirectionURL = authz.getRedirectionURL(object, baseErrorURL)

return errorRedirectionURL
}

func (authz *Authz) authn(ctx *middlewares.AutheliaCtx, provider *session.Session, object *authorization.Object) (authn *Authn, strategy AuthnStrategy, err error) {
for _, strategy = range authz.strategies {
if authn, err = strategy.Get(ctx, provider, object); err != nil {
Expand Down
4 changes: 4 additions & 0 deletions internal/handlers/handler_authz_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,18 +128,22 @@ func (b *AuthzBuilder) Build() (authz *Authz) {
authz.config.StatusCodeBadRequest = fasthttp.StatusUnauthorized
authz.handleGetObject = handleAuthzGetObjectLegacy
authz.handleUnauthorized = handleAuthzUnauthorizedLegacy
authz.handleForbidden = handleAuthzForbiddenLegacy
authz.handleGetAutheliaURL = handleAuthzPortalURLLegacy
case AuthzImplForwardAuth:
authz.handleGetObject = handleAuthzGetObjectForwardAuth
authz.handleUnauthorized = handleAuthzUnauthorizedForwardAuth
authz.handleForbidden = handleAuthzForbiddenForwardAuth
authz.handleGetAutheliaURL = handleAuthzPortalURLFromQuery
case AuthzImplAuthRequest:
authz.handleGetObject = handleAuthzGetObjectAuthRequest
authz.handleUnauthorized = handleAuthzUnauthorizedAuthRequest
authz.handleForbidden = handleAuthzForbiddenAuthRequest
authz.handleGetAutheliaURL = handleAuthzPortalURLFromQuery
case AuthzImplExtAuthz:
authz.handleGetObject = handleAuthzGetObjectExtAuthz
authz.handleUnauthorized = handleAuthzUnauthorizedExtAuthz
authz.handleForbidden = handleAuthzForbiddenExtAuthz
authz.handleGetAutheliaURL = handleAuthzPortalURLFromHeader
}

Expand Down
24 changes: 24 additions & 0 deletions internal/handlers/handler_authz_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,27 @@ func hasInvalidMethodCharacters(v []byte) bool {

return false
}

func isNotRequestingWebpage(ctx *middlewares.AutheliaCtx) bool {
return ctx.IsXHR() || !ctx.AcceptsMIME("text/html")
}

func deriveStatusCodeFromAuthnMethod(authn *Authn) (statusCode int) {
switch authn.Object.Method {
case fasthttp.MethodGet, fasthttp.MethodOptions, fasthttp.MethodHead:
return fasthttp.StatusFound
default:
return fasthttp.StatusSeeOther
}
}

func handleAuthzSpecialRedirect(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL, statusCode int) {
ctx.Logger.Infof(logFmtAuthzRedirect, authn.Object.String(), authn.Method, authn.Username, statusCode, redirectionURL)

switch authn.Object.Method {
case fasthttp.MethodHead:
ctx.SpecialRedirectNoBody(redirectionURL.String(), statusCode)
default:
ctx.SpecialRedirect(redirectionURL.String(), statusCode)
}
}
11 changes: 4 additions & 7 deletions internal/handlers/handler_authz_impl_authrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,9 @@ func handleAuthzGetObjectAuthRequest(ctx *middlewares.AutheliaCtx) (object autho
}

func handleAuthzUnauthorizedAuthRequest(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL) {
ctx.Logger.Infof(logFmtAuthzRedirect, authn.Object.URL.String(), authn.Method, authn.Username, fasthttp.StatusUnauthorized, redirectionURL)
handleAuthzSpecialRedirect(ctx, authn, redirectionURL, fasthttp.StatusUnauthorized)
}

switch authn.Object.Method {
case fasthttp.MethodHead:
ctx.SpecialRedirectNoBody(redirectionURL.String(), fasthttp.StatusUnauthorized)
default:
ctx.SpecialRedirect(redirectionURL.String(), fasthttp.StatusUnauthorized)
}
func handleAuthzForbiddenAuthRequest(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL) {
handleAuthzSpecialRedirect(ctx, authn, redirectionURL, fasthttp.StatusForbidden)
}
31 changes: 16 additions & 15 deletions internal/handlers/handler_authz_impl_extauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,25 @@ func handleAuthzUnauthorizedExtAuthz(ctx *middlewares.AutheliaCtx, authn *Authn,
statusCode int
)

switch {
case ctx.IsXHR() || !ctx.AcceptsMIME("text/html"):
if isNotRequestingWebpage(ctx) {
statusCode = fasthttp.StatusUnauthorized
default:
switch authn.Object.Method {
case fasthttp.MethodGet, fasthttp.MethodOptions, fasthttp.MethodHead:
statusCode = fasthttp.StatusFound
default:
statusCode = fasthttp.StatusSeeOther
}
} else {
statusCode = deriveStatusCodeFromAuthnMethod(authn)
}

ctx.Logger.Infof(logFmtAuthzRedirect, authn.Object.String(), authn.Method, authn.Username, statusCode, redirectionURL)
handleAuthzSpecialRedirect(ctx, authn, redirectionURL, statusCode)
}

func handleAuthzForbiddenExtAuthz(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL) {
var (
statusCode int
)

switch authn.Object.Method {
case fasthttp.MethodHead:
ctx.SpecialRedirectNoBody(redirectionURL.String(), statusCode)
default:
ctx.SpecialRedirect(redirectionURL.String(), statusCode)
if isNotRequestingWebpage(ctx) {
statusCode = fasthttp.StatusForbidden
} else {
statusCode = deriveStatusCodeFromAuthnMethod(authn)
}

handleAuthzSpecialRedirect(ctx, authn, redirectionURL, statusCode)
}
31 changes: 16 additions & 15 deletions internal/handlers/handler_authz_impl_forwardauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,25 @@ func handleAuthzUnauthorizedForwardAuth(ctx *middlewares.AutheliaCtx, authn *Aut
statusCode int
)

switch {
case ctx.IsXHR() || !ctx.AcceptsMIME("text/html"):
if isNotRequestingWebpage(ctx) {
statusCode = fasthttp.StatusUnauthorized
default:
switch authn.Object.Method {
case fasthttp.MethodGet, fasthttp.MethodOptions, fasthttp.MethodHead:
statusCode = fasthttp.StatusFound
default:
statusCode = fasthttp.StatusSeeOther
}
} else {
statusCode = deriveStatusCodeFromAuthnMethod(authn)
}

ctx.Logger.Infof(logFmtAuthzRedirect, authn.Object.String(), authn.Method, authn.Username, statusCode, redirectionURL)
handleAuthzSpecialRedirect(ctx, authn, redirectionURL, statusCode)
}

func handleAuthzForbiddenForwardAuth(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL) {
var (
statusCode int
)

switch authn.Object.Method {
case fasthttp.MethodHead:
ctx.SpecialRedirectNoBody(redirectionURL.String(), statusCode)
default:
ctx.SpecialRedirect(redirectionURL.String(), statusCode)
if isNotRequestingWebpage(ctx) {
statusCode = fasthttp.StatusForbidden
} else {
statusCode = deriveStatusCodeFromAuthnMethod(authn)
}

handleAuthzSpecialRedirect(ctx, authn, redirectionURL, statusCode)
}
49 changes: 35 additions & 14 deletions internal/handlers/handler_authz_impl_legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,33 +38,54 @@ func handleAuthzUnauthorizedLegacy(ctx *middlewares.AutheliaCtx, authn *Authn, r

if authn.Type == AuthnTypeAuthorization {
handleAuthzUnauthorizedAuthorizationBasic(ctx, authn)

return
}

switch {
case ctx.IsXHR() || !ctx.AcceptsMIME("text/html") || redirectionURL == nil:
case isNotRequestingWebpage(ctx) || redirectionURL == nil:
statusCode = fasthttp.StatusUnauthorized
default:
switch authn.Object.Method {
case fasthttp.MethodGet, fasthttp.MethodOptions, fasthttp.MethodHead, "":
if authn.Object.Method == "" {
statusCode = fasthttp.StatusFound
default:
statusCode = fasthttp.StatusSeeOther
} else {
statusCode = deriveStatusCodeFromAuthnMethod(authn)
}
}

if redirectionURL != nil {
ctx.Logger.Infof(logFmtAuthzRedirect, authn.Object.URL.String(), authn.Method, authn.Username, statusCode, redirectionURL)

switch authn.Object.Method {
case fasthttp.MethodHead:
ctx.SpecialRedirectNoBody(redirectionURL.String(), statusCode)
default:
ctx.SpecialRedirect(redirectionURL.String(), statusCode)
}
handleAuthzSpecialRedirect(ctx, authn, redirectionURL, statusCode)
} else {
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d", authn.Object.URL.String(), authn.Method, authn.Username, statusCode)
ctx.ReplyUnauthorized()
}
}

func handleAuthzForbiddenLegacy(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL) {
var (
statusCode int
)

if authn.Type == AuthnTypeAuthorization {
ctx.Logger.Infof("Access to %s (method %s) is forbidden for user %s, responding with status code %d", authn.Object.URL.String(), authn.Method, authn.Username, fasthttp.StatusForbidden)
ctx.ReplyForbidden()
return
}

switch {
case isNotRequestingWebpage(ctx) || redirectionURL == nil:
statusCode = fasthttp.StatusForbidden
default:
if authn.Object.Method == "" {
statusCode = fasthttp.StatusFound
} else {
statusCode = deriveStatusCodeFromAuthnMethod(authn)
}
}

if redirectionURL != nil {
handleAuthzSpecialRedirect(ctx, authn, redirectionURL, statusCode)
} else {
ctx.Logger.Infof("Access to %s (method %s) is forbidden for user %s, responding with status code %d", authn.Object.URL.String(), authn.Method, authn.Username, statusCode)
ctx.ReplyForbidden()
}
}
Loading

0 comments on commit bd9e477

Please sign in to comment.