Skip to content

Commit

Permalink
feat(jarm): jwt-secured authorization response mode (#29)
Browse files Browse the repository at this point in the history
This implements the JARM specification. See https://openid.net/specs/oauth-v2-jarm.html. In addition it refactors the Response Mode Handlers to entirely be served by a new Configurator Provider.
  • Loading branch information
james-d-elliott authored Dec 22, 2023
1 parent 2280bff commit 2b64360
Show file tree
Hide file tree
Showing 26 changed files with 1,039 additions and 476 deletions.
64 changes: 22 additions & 42 deletions authorize_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,60 +12,40 @@ import (
"authelia.com/provider/oauth2/internal/consts"
)

func (f *Fosite) WriteAuthorizeError(ctx context.Context, rw http.ResponseWriter, ar AuthorizeRequester, err error) {
func (f *Fosite) WriteAuthorizeError(ctx context.Context, rw http.ResponseWriter, requester AuthorizeRequester, err error) {
rw.Header().Set(consts.HeaderCacheControl, consts.CacheControlNoStore)
rw.Header().Set(consts.HeaderPragma, consts.PragmaNoCache)

if f.ResponseModeHandler(ctx).ResponseModes().Has(ar.GetResponseMode()) {
f.ResponseModeHandler(ctx).WriteAuthorizeError(ctx, rw, ar, err)
return
}

rfc := ErrorToRFC6749Error(err).WithLegacyFormat(f.Config.GetUseLegacyErrorFormat(ctx)).WithExposeDebug(f.Config.GetSendDebugMessagesToClients(ctx)).WithLocalizer(f.Config.GetMessageCatalog(ctx), getLangFromRequester(ar))
if !ar.IsRedirectURIValid() {
rw.Header().Set(consts.HeaderContentType, consts.ContentTypeApplicationJSON)
for _, handler := range f.ResponseModeHandlers(ctx) {
if handler.ResponseModes().Has(requester.GetResponseMode()) {
handler.WriteAuthorizeError(ctx, rw, requester, err)

js, err := json.Marshal(rfc)
if err != nil {
if f.Config.GetSendDebugMessagesToClients(ctx) {
errorMessage := EscapeJSONString(err.Error())
http.Error(rw, fmt.Sprintf(`{"error":"server_error","error_description":"%s"}`, errorMessage), http.StatusInternalServerError)
} else {
http.Error(rw, `{"error":"server_error"}`, http.StatusInternalServerError)
}
return
}

rw.WriteHeader(rfc.CodeField)
_, _ = rw.Write(js)
return
}

redirectURI := ar.GetRedirectURI()
f.handleWriteAuthorizeErrorJSON(ctx, rw, ErrServerError.WithHint("The Authorization Server was unable to process the requested Response Mode."))
}

// The endpoint URI MUST NOT include a fragment component.
redirectURI.Fragment = ""
func (f *Fosite) handleWriteAuthorizeErrorJSON(ctx context.Context, rw http.ResponseWriter, rfc *RFC6749Error) {
rw.Header().Set(consts.HeaderContentType, consts.ContentTypeApplicationJSON)

errors := rfc.ToValues()
errors.Set(consts.FormParameterState, ar.GetState())
var (
data []byte
err error
)

var redirectURIString string
if ar.GetResponseMode() == ResponseModeFormPost {
rw.Header().Set(consts.HeaderContentType, consts.ContentTypeTextHTML)
WriteAuthorizeFormPostResponse(redirectURI.String(), errors, GetPostFormHTMLTemplate(ctx, f), rw)
return
} else if ar.GetResponseMode() == ResponseModeFragment {
redirectURIString = redirectURI.String() + "#" + errors.Encode()
} else {
for key, values := range redirectURI.Query() {
for _, value := range values {
errors.Add(key, value)
}
if data, err = json.Marshal(rfc); err != nil {
if f.Config.GetSendDebugMessagesToClients(ctx) {
errorMessage := EscapeJSONString(err.Error())
http.Error(rw, fmt.Sprintf(`{"error":"server_error","error_description":"%s"}`, errorMessage), http.StatusInternalServerError)
} else {
http.Error(rw, `{"error":"server_error"}`, http.StatusInternalServerError)
}
redirectURI.RawQuery = errors.Encode()
redirectURIString = redirectURI.String()

return
}

rw.Header().Set(consts.HeaderLocation, redirectURIString)
rw.WriteHeader(http.StatusSeeOther)
rw.WriteHeader(rfc.CodeField)
_, _ = rw.Write(data)
}
152 changes: 77 additions & 75 deletions authorize_error_test.go

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions authorize_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,10 @@ func WriteAuthorizeFormPostResponse(redirectURL string, parameters url.Values, t
})
}

func GetPostFormHTMLTemplate(ctx context.Context, f *Fosite) *template.Template {
if t := f.Config.GetFormPostHTMLTemplate(ctx); t != nil {
func GetPostFormHTMLTemplate(ctx context.Context, c FormPostHTMLTemplateProvider) *template.Template {
if t := c.GetFormPostHTMLTemplate(ctx); t != nil {
return t
}

return DefaultFormPostTemplate
}
12 changes: 8 additions & 4 deletions authorize_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ import (
type ResponseModeType string

const (
ResponseModeDefault = ResponseModeType("")
ResponseModeFormPost = ResponseModeType(consts.ResponseModeFormPost)
ResponseModeQuery = ResponseModeType(consts.ResponseModeQuery)
ResponseModeFragment = ResponseModeType(consts.ResponseModeFragment)
ResponseModeDefault = ResponseModeType("")
ResponseModeFormPost = ResponseModeType(consts.ResponseModeFormPost)
ResponseModeQuery = ResponseModeType(consts.ResponseModeQuery)
ResponseModeFragment = ResponseModeType(consts.ResponseModeFragment)
ResponseModeFormPostJWT = ResponseModeType(consts.ResponseModeFormPostJWT)
ResponseModeQueryJWT = ResponseModeType(consts.ResponseModeQueryJWT)
ResponseModeFragmentJWT = ResponseModeType(consts.ResponseModeFragmentJWT)
ResponseModeJWT = ResponseModeType(consts.ResponseModeJWT)
)

// AuthorizeRequest is an implementation of AuthorizeRequester
Expand Down
28 changes: 12 additions & 16 deletions authorize_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,25 +228,19 @@ func (f *Fosite) validateResponseTypes(r *http.Request, request *AuthorizeReques
}

func (f *Fosite) ParseResponseMode(ctx context.Context, r *http.Request, request *AuthorizeRequest) error {
switch responseMode := r.Form.Get(consts.FormParameterResponseMode); responseMode {
case string(ResponseModeDefault):
request.ResponseMode = ResponseModeDefault
case string(ResponseModeFragment):
request.ResponseMode = ResponseModeFragment
case string(ResponseModeQuery):
request.ResponseMode = ResponseModeQuery
case string(ResponseModeFormPost):
request.ResponseMode = ResponseModeFormPost
default:
rm := ResponseModeType(responseMode)
if f.ResponseModeHandler(ctx).ResponseModes().Has(rm) {
request.ResponseMode = rm
break
m := r.Form.Get(consts.FormParameterResponseMode)

for _, handler := range f.ResponseModeHandlers(ctx) {
mode := ResponseModeType(m)

if handler.ResponseModes().Has(mode) {
request.ResponseMode = mode

return nil
}
return errorsx.WithStack(ErrUnsupportedResponseMode.WithHintf("Request with unsupported response_mode \"%s\".", responseMode))
}

return nil
return errorsx.WithStack(ErrUnsupportedResponseMode.WithHintf("Request with unsupported response_mode \"%s\".", m))
}

func (f *Fosite) validateResponseMode(r *http.Request, request *AuthorizeRequest) error {
Expand Down Expand Up @@ -334,6 +328,7 @@ func (f *Fosite) newAuthorizeRequest(ctx context.Context, r *http.Request, isPAR
if err := r.ParseMultipartForm(1 << 20); err != nil && err != http.ErrNotMultipart {
return request, errorsx.WithStack(ErrInvalidRequest.WithHint("Unable to parse HTTP body, make sure to send a properly formatted form request body.").WithWrap(err).WithDebug(err.Error()))
}

request.Form = r.Form

// Save state to the request to be returned in error conditions (https://github.com/ory/hydra/issues/1642)
Expand All @@ -355,6 +350,7 @@ func (f *Fosite) newAuthorizeRequest(ctx context.Context, r *http.Request, isPAR
if err != nil {
return request, errorsx.WithStack(ErrInvalidClient.WithHint("The requested OAuth 2.0 Client does not exist.").WithWrap(err).WithDebug(err.Error()))
}

request.Client = client

// Now that the base fields (state and client) are populated, we extract all the information
Expand Down
48 changes: 6 additions & 42 deletions authorize_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,16 @@ import (
"authelia.com/provider/oauth2/internal/consts"
)

func (f *Fosite) WriteAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, ar AuthorizeRequester, resp AuthorizeResponder) {
// Set custom headers, e.g. "X-MySuperCoolCustomHeader" or "X-DONT-CACHE-ME"...
wh := rw.Header()
rh := resp.GetHeader()
for k := range rh {
wh.Set(k, rh.Get(k))
}

wh.Set(consts.HeaderCacheControl, consts.CacheControlNoStore)
wh.Set(consts.HeaderPragma, consts.PragmaNoCache)

redir := ar.GetRedirectURI()
switch rm := ar.GetResponseMode(); rm {
case ResponseModeFormPost:
//form_post
rw.Header().Add(consts.HeaderContentType, consts.ContentTypeTextHTML)
WriteAuthorizeFormPostResponse(redir.String(), resp.GetParameters(), GetPostFormHTMLTemplate(ctx, f), rw)
return
case ResponseModeQuery, ResponseModeDefault:
// Explicit grants
q := redir.Query()
rq := resp.GetParameters()
for k := range rq {
q.Set(k, rq.Get(k))
}
redir.RawQuery = q.Encode()
sendRedirect(redir.String(), rw)
return
case ResponseModeFragment:
// Implicit grants
// The endpoint URI MUST NOT include a fragment component.
redir.Fragment = ""
func (f *Fosite) WriteAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, requester AuthorizeRequester, responder AuthorizeResponder) {
for _, handler := range f.ResponseModeHandlers(ctx) {
if handler.ResponseModes().Has(requester.GetResponseMode()) {
handler.WriteAuthorizeResponse(ctx, rw, requester, responder)

u := redir.String()
fr := resp.GetParameters()
if len(fr) > 0 {
u = u + "#" + fr.Encode()
}
sendRedirect(u, rw)
return
default:
if f.ResponseModeHandler(ctx).ResponseModes().Has(rm) {
f.ResponseModeHandler(ctx).WriteAuthorizeResponse(ctx, rw, ar, resp)
return
}
}

f.handleWriteAuthorizeErrorJSON(ctx, rw, ErrServerError.WithHint("The Authorization Server was unable to process the requested Response Mode."))
}

// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
Expand Down
Loading

0 comments on commit 2b64360

Please sign in to comment.