Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [#615] i18n support added #627

Merged
merged 3 commits into from
Oct 29, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions access_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,21 @@ import (
"net/http"
)

func (f *Fosite) WriteAccessError(rw http.ResponseWriter, _ AccessRequester, err error) {
f.writeJsonError(rw, err)
func (f *Fosite) WriteAccessError(rw http.ResponseWriter, req AccessRequester, err error) {
f.writeJsonError(rw, req, err)
}

func (f *Fosite) writeJsonError(rw http.ResponseWriter, err error) {
func (f *Fosite) writeJsonError(rw http.ResponseWriter, requester AccessRequester, err error) {
rw.Header().Set("Content-Type", "application/json;charset=UTF-8")
rw.Header().Set("Cache-Control", "no-store")
rw.Header().Set("Pragma", "no-cache")

rfcerr := ErrorToRFC6749Error(err).WithLegacyFormat(f.UseLegacyErrorFormat).WithExposeDebug(f.SendDebugMessagesToClients)

if requester != nil {
rfcerr = rfcerr.WithLocalizer(f.MessageCatalog, getLangFromRequester(requester))
}

js, err := json.Marshal(rfcerr)
if err != nil {
if f.SendDebugMessagesToClients {
Expand Down
2 changes: 2 additions & 0 deletions access_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"net/http"
"strings"

"github.com/ory/fosite/i18n"
"github.com/ory/x/errorsx"

"github.com/pkg/errors"
Expand Down Expand Up @@ -58,6 +59,7 @@ import (
// in Section 3.2.1.
func (f *Fosite) NewAccessRequest(ctx context.Context, r *http.Request, session Session) (AccessRequester, error) {
accessRequest := NewAccessRequest(session)
accessRequest.Request.Lang = i18n.GetLangFromRequest(f.MessageCatalog, r)
aeneasr marked this conversation as resolved.
Show resolved Hide resolved

ctx = context.WithValue(ctx, RequestContextKey, r)
ctx = context.WithValue(ctx, AccessRequestContextKey, accessRequest)
Expand Down
2 changes: 1 addition & 1 deletion access_response_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (f *Fosite) NewAccessResponse(ctx context.Context, requester AccessRequeste
}

if response.GetAccessToken() == "" || response.GetTokenType() == "" {
return nil, errorsx.WithStack(ErrServerError.WithHint("An internal server occurred while trying to complete the request.").WithDebug("Access token or token type not set by TokenEndpointHandlers."))
return nil, errorsx.WithStack(ErrServerError.WithHint("An internal server occurred while trying to complete the request.").WithDebug("Access token or token type not set by TokenEndpointHandlers.").WithLocalizer(f.MessageCatalog, getLangFromRequester(requester)))
}

return response, nil
Expand Down
2 changes: 1 addition & 1 deletion authorize_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (f *Fosite) WriteAuthorizeError(rw http.ResponseWriter, ar AuthorizeRequest
return
}

rfcerr := ErrorToRFC6749Error(err).WithLegacyFormat(f.UseLegacyErrorFormat).WithExposeDebug(f.SendDebugMessagesToClients)
rfcerr := ErrorToRFC6749Error(err).WithLegacyFormat(f.UseLegacyErrorFormat).WithExposeDebug(f.SendDebugMessagesToClients).WithLocalizer(f.MessageCatalog, getLangFromRequester(ar))
if !ar.IsRedirectURIValid() {
rw.Header().Set("Content-Type", "application/json;charset=UTF-8")

Expand Down
2 changes: 2 additions & 0 deletions authorize_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"net/http"
"strings"

"github.com/ory/fosite/i18n"
"github.com/ory/fosite/token/jwt"
"github.com/ory/x/errorsx"
"gopkg.in/square/go-jose.v2"
Expand Down Expand Up @@ -276,6 +277,7 @@ func (f *Fosite) validateResponseMode(r *http.Request, request *AuthorizeRequest

func (f *Fosite) NewAuthorizeRequest(ctx context.Context, r *http.Request) (AuthorizeRequester, error) {
request := NewAuthorizeRequest()
request.Request.Lang = i18n.GetLangFromRequest(f.MessageCatalog, r)

ctx = context.WithValue(ctx, RequestContextKey, r)
ctx = context.WithValue(ctx, AuthorizeRequestContextKey, request)
Expand Down
3 changes: 2 additions & 1 deletion authorize_response_writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"

"github.com/ory/fosite"
. "github.com/ory/fosite"
. "github.com/ory/fosite/internal"
)
Expand Down Expand Up @@ -100,7 +101,7 @@ func TestNewAuthorizeResponse(t *testing.T) {
ar.EXPECT().GetResponseTypes().Return([]string{"token", "code"})
},
isErr: true,
expectErr: ErrUnsupportedResponseMode.WithHintf("Insecure response_mode '%s' for the response_type '%s'.", ResponseModeQuery, []string{"token", "code"}),
expectErr: ErrUnsupportedResponseMode.WithHintf("Insecure response_mode '%s' for the response_type '%s'.", ResponseModeQuery, fosite.Arguments{"token", "code"}),
},
} {
c.mock()
Expand Down
1 change: 1 addition & 0 deletions compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func Compose(config *Config, storage interface{}, strategy interface{}, hasher f
UseLegacyErrorFormat: config.UseLegacyErrorFormat,
ClientAuthenticationStrategy: config.GetClientAuthenticationStrategy(),
ResponseModeHandlerExtension: config.ResponseModeHandlerExtension,
MessageCatalog: config.MessageCatalog,
}

for _, factory := range factories {
Expand Down
4 changes: 4 additions & 0 deletions compose/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"time"

"github.com/ory/fosite"
"github.com/ory/fosite/i18n"
)

type Config struct {
Expand Down Expand Up @@ -117,6 +118,9 @@ type Config struct {

// ResponseModeHandlerExtension provides a handler for custom response modes
ResponseModeHandlerExtension fosite.ResponseModeHandler

// MessageCatalog is the message bundle used for i18n
MessageCatalog i18n.MessageCatalog
}

// GetScopeStrategy returns the scope strategy to be used. Defaults to glob scope strategy.
Expand Down
42 changes: 40 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import (
"net/url"
"strings"

"github.com/ory/fosite/i18n"
"github.com/ory/x/errorsx"
"golang.org/x/text/language"

stderr "errors"

Expand Down Expand Up @@ -267,6 +269,12 @@ type (
cause error
useLegacyFormat bool
exposeDebug bool

// Fields for globalization
hintIDField string
hintArgs []interface{}
catalog i18n.MessageCatalog
lang language.Tag
}
stackTracer interface {
StackTrace() errors.StackTrace
Expand Down Expand Up @@ -373,15 +381,29 @@ func (e *RFC6749Error) Cause() error {
}

func (e *RFC6749Error) WithHintf(hint string, args ...interface{}) *RFC6749Error {
return e.WithHint(fmt.Sprintf(hint, args...))
err := *e
err.hintIDField = hint
err.hintArgs = args
err.HintField = fmt.Sprintf(hint, args...)
return &err
}

func (e *RFC6749Error) WithHint(hint string) *RFC6749Error {
err := *e
err.hintIDField = hint
err.HintField = hint
return &err
}

// WithHintIDOrDefault accepts the ID of the hint message
func (e *RFC6749Error) WithHintIDOrDefaultf(ID string, def string, args ...interface{}) *RFC6749Error {
aeneasr marked this conversation as resolved.
Show resolved Hide resolved
err := *e
err.hintIDField = ID
err.hintArgs = args
err.HintField = fmt.Sprintf(def, args...)
return &err
}

func (e *RFC6749Error) Debug() string {
return e.DebugField
}
Expand All @@ -402,6 +424,13 @@ func (e *RFC6749Error) WithDescription(description string) *RFC6749Error {
return &err
}

func (e *RFC6749Error) WithLocalizer(catalog i18n.MessageCatalog, lang language.Tag) *RFC6749Error {
err := *e
err.catalog = catalog
err.lang = lang
return &err
}

// Sanitize strips the debug field
//
// Deprecated: Use WithExposeDebug instead.
Expand All @@ -420,7 +449,8 @@ func (e *RFC6749Error) WithExposeDebug(exposeDebug bool) *RFC6749Error {

// GetDescription returns a more description description, combined with hint and debug (when available).
func (e *RFC6749Error) GetDescription() string {
description := e.DescriptionField
description := i18n.GetMessageOrDefault(e.catalog, e.ErrorField, e.lang, e.DescriptionField)
e.computeHintField()
if e.HintField != "" {
description += " " + e.HintField
}
Expand Down Expand Up @@ -499,3 +529,11 @@ func (e *RFC6749Error) ToValues() url.Values {

return values
}

func (e *RFC6749Error) computeHintField() {
if e.hintIDField == "" {
return
}

e.HintField = i18n.GetMessageOrDefault(e.catalog, e.hintIDField, e.lang, e.HintField, e.hintArgs...)
}
61 changes: 61 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ package fosite
import (
"testing"

"github.com/ory/fosite/i18n"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
)

func TestRFC6749Error(t *testing.T) {
Expand All @@ -44,3 +46,62 @@ func TestRFC6749Error(t *testing.T) {
assert.Empty(t, wrap.StackTrace())
})
}

func TestErrorI18N(t *testing.T) {
catalog := i18n.NewDefaultMessageCatalog([]*i18n.DefaultLocaleBundle{
{
LangTag: "en",
Messages: []*i18n.DefaultMessage{
{
ID: "access_denied",
FormattedMessage: "The resource owner or authorization server denied the request.",
},
{
ID: "badRequestMethod",
FormattedMessage: "HTTP method is '%s', expected 'POST'.",
},
},
},
{
LangTag: "es",
Messages: []*i18n.DefaultMessage{
{
ID: "access_denied",
FormattedMessage: "El propietario del recurso o el servidor de autorización denegó la solicitud.",
},
{
ID: "HTTP method is '%s', expected 'POST'.",
FormattedMessage: "El método HTTP es '%s', esperado 'POST'.",
},
{
ID: "Unable to parse HTTP body, make sure to send a properly formatted form request body.",
FormattedMessage: "No se puede analizar el cuerpo HTTP, asegúrese de enviar un cuerpo de solicitud de formulario con el formato adecuado.",
},
{
ID: "badRequestMethod",
FormattedMessage: "El método HTTP es '%s', esperado 'POST'.",
},
},
},
})

t.Run("case=legacy", func(t *testing.T) {
err := ErrAccessDenied.WithLocalizer(catalog, language.Spanish).WithHintf("HTTP method is '%s', expected 'POST'.", "GET")
assert.EqualValues(t, "El propietario del recurso o el servidor de autorización denegó la solicitud. El método HTTP es 'GET', esperado 'POST'.", err.GetDescription())
})

t.Run("case=unsupported_locale_legacy", func(t *testing.T) {
err := ErrAccessDenied.WithLocalizer(catalog, language.Afrikaans).WithHintf("HTTP method is '%s', expected 'POST'.", "GET")
assert.EqualValues(t, "The resource owner or authorization server denied the request. HTTP method is 'GET', expected 'POST'.", err.GetDescription())
})

t.Run("case=simple", func(t *testing.T) {
err := ErrAccessDenied.WithLocalizer(catalog, language.Spanish).WithHintIDOrDefaultf("badRequestMethod", "HTTP method is '%s', expected 'POST'.", "GET")
assert.EqualValues(t, "El propietario del recurso o el servidor de autorización denegó la solicitud. El método HTTP es 'GET', esperado 'POST'.", err.GetDescription())
})

t.Run("case=unsupported_locale", func(t *testing.T) {
err := ErrAccessDenied.WithLocalizer(catalog, language.Afrikaans).WithHintIDOrDefaultf("badRequestMethod", "HTTP method is '%s', expected 'POST'.", "GET")
assert.EqualValues(t, "The resource owner or authorization server denied the request. HTTP method is 'GET', expected 'POST'.", err.GetDescription())
})
}
5 changes: 5 additions & 0 deletions fosite.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"html/template"
"net/http"
"reflect"

"github.com/ory/fosite/i18n"
)

// AuthorizeEndpointHandlers is a list of AuthorizeEndpointHandler
Expand Down Expand Up @@ -115,6 +117,9 @@ type Fosite struct {
ClientAuthenticationStrategy ClientAuthenticationStrategy

ResponseModeHandlerExtension ResponseModeHandler

// MessageCatalog is the catalog of messages used for i18n
MessageCatalog i18n.MessageCatalog
}

const MinParameterEntropy = 8
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/text v0.3.3
gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614
)

Expand Down
Loading