initial split from 04756eee65e20001b3a44af6249b60c52af446b7
This commit is contained in:
264
options/options.go
Normal file
264
options/options.go
Normal file
@ -0,0 +1,264 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ClaimsContextKeyName is the type for they key value used to pass claims using request context.
|
||||
// Using separate type because of the following: https://staticcheck.io/docs/checks#SA1029
|
||||
type ClaimsContextKeyName string
|
||||
|
||||
// DefaultClaimsContextKeyName is of type ClaimsContextKeyName and defaults to "claims"
|
||||
const DefaultClaimsContextKeyName ClaimsContextKeyName = "claims"
|
||||
|
||||
// ErrorHandler is called by the middleware if not nil
|
||||
type ErrorHandler func(description ErrorDescription, err error)
|
||||
|
||||
// ErrorDescription is used to pass the description of the error to ErrorHandler
|
||||
type ErrorDescription string
|
||||
|
||||
const (
|
||||
// GetTokenErrorDescription is returned to ErrorHandler if the middleware is unable to get a token from the request
|
||||
GetTokenErrorDescription ErrorDescription = "unable to get token string"
|
||||
// ParseTokenErrorDescription is returned to ErrorHandler if the middleware is unable to parse the token extracted from the request
|
||||
ParseTokenErrorDescription ErrorDescription = "unable to parse token string"
|
||||
// ConvertTokenErrorDescription is returned to ErrorHandler if the middleware is unable to convert the token to a map
|
||||
ConvertTokenErrorDescription ErrorDescription = "unable to convert token to map"
|
||||
)
|
||||
|
||||
// Options defines the options for OIDC Middleware.
|
||||
type Options struct {
|
||||
Issuer string
|
||||
DiscoveryUri string
|
||||
JwksUri string
|
||||
JwksFetchTimeout time.Duration
|
||||
JwksRateLimit uint
|
||||
FallbackSignatureAlgorithm string
|
||||
AllowedTokenDrift time.Duration
|
||||
LazyLoadJwks bool
|
||||
RequiredTokenType string
|
||||
RequiredAudience string
|
||||
RequiredClaims map[string]interface{}
|
||||
DisableKeyID bool
|
||||
HttpClient *http.Client
|
||||
TokenString [][]TokenStringOption
|
||||
ClaimsContextKeyName ClaimsContextKeyName
|
||||
ErrorHandler ErrorHandler
|
||||
}
|
||||
|
||||
// New takes Option setters and returns an Options pointer.
|
||||
// Mainly used by the internal functions and most likely not
|
||||
// needed by any external application using this library.
|
||||
func New(setters ...Option) *Options {
|
||||
opts := &Options{
|
||||
JwksFetchTimeout: 5 * time.Second,
|
||||
JwksRateLimit: 1,
|
||||
AllowedTokenDrift: 10 * time.Second,
|
||||
HttpClient: http.DefaultClient,
|
||||
ClaimsContextKeyName: DefaultClaimsContextKeyName,
|
||||
}
|
||||
|
||||
for _, setter := range setters {
|
||||
setter(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// Option returns a function that modifies an Options pointer.
|
||||
type Option func(*Options)
|
||||
|
||||
// WithIssuer sets the Issuer parameter for Options.
|
||||
// Issuer is the authority that issues the tokens
|
||||
func WithIssuer(opt string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Issuer = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithDiscoveryUri sets the Issuer parameter for an Options pointer.
|
||||
// DiscoveryUri is where the `jwks_uri` will be grabbed
|
||||
// Defaults to `fmt.Sprintf("%s/.well-known/openid-configuration", strings.TrimSuffix(issuer, "/"))`
|
||||
func WithDiscoveryUri(opt string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.DiscoveryUri = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithJwksUri sets the JwksUri parameter for an Options pointer.
|
||||
// JwksUri is used to download the public key(s)
|
||||
// Defaults to the `jwks_uri` from the response of DiscoveryUri
|
||||
func WithJwksUri(opt string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.JwksUri = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithJwksFetchTimeout sets the JwksFetchTimeout parameter for an Options pointer.
|
||||
// JwksFetchTimeout sets the context timeout when downloading the jwks
|
||||
// Defaults to 5 seconds
|
||||
func WithJwksFetchTimeout(opt time.Duration) Option {
|
||||
return func(opts *Options) {
|
||||
opts.JwksFetchTimeout = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithJwksRateLimit sets the JwksFetchTimeout parameter for an Options pointer.
|
||||
// JwksRateLimit takes an uint and makes sure that the jwks will at a maximum
|
||||
// be requested these many times per second.
|
||||
// Defaults to 1 (Request Per Second)
|
||||
// Please observe: Requests that force update of jwks (like wrong keyID) will be rate limited
|
||||
func WithJwksRateLimit(opt uint) Option {
|
||||
return func(opts *Options) {
|
||||
opts.JwksRateLimit = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithFallbackSignatureAlgorithm sets the FallbackSignatureAlgorithm parameter for an Options pointer.
|
||||
// FallbackSignatureAlgorithm needs to be used when the jwks doesn't contain the alg key.
|
||||
// If not specified and jwks doesn't contain alg key, will default to:
|
||||
// - RS256 for key type (kty) RSA
|
||||
// - ES256 for key type (kty) EC
|
||||
//
|
||||
// When specified and jwks contains alg key, alg key from jwks will be used.
|
||||
//
|
||||
// Example values (one of them): RS256 RS384 RS512 ES256 ES384 ES512
|
||||
func WithFallbackSignatureAlgorithm(opt string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.FallbackSignatureAlgorithm = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithAllowedTokenDrift sets the AllowedTokenDrift parameter for an Options pointer.
|
||||
// AllowedTokenDrift adds the duration to the token expiration to allow
|
||||
// for time drift between parties.
|
||||
// Defaults to 10 seconds
|
||||
func WithAllowedTokenDrift(opt time.Duration) Option {
|
||||
return func(opts *Options) {
|
||||
opts.AllowedTokenDrift = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithLazyLoadJwks sets the LazyLoadJwks parameter for an Options pointer.
|
||||
// LazyLoadJwks makes it possible to use OIDC Discovery without being
|
||||
// able to load the keys at startup.
|
||||
// Default setting is disabled.
|
||||
// Please observe: If enabled, it will always load even though settings
|
||||
// may be wrong / not working.
|
||||
func WithLazyLoadJwks(opt bool) Option {
|
||||
return func(opts *Options) {
|
||||
opts.LazyLoadJwks = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithRequiredTokenType sets the RequiredTokenType parameter for an Options pointer.
|
||||
// RequiredTokenType is used if only specific tokens should be allowed.
|
||||
// Default is empty string `""` and means all token types are allowed.
|
||||
// Use case could be to configure this if the TokenType (set in the header of the JWT)
|
||||
// should be `JWT` or maybe even `JWT+AT` to differentiate between access tokens and
|
||||
// id tokens. Not all providers support or use this.
|
||||
func WithRequiredTokenType(opt string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.RequiredTokenType = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithRequiredAudience sets the RequiredAudience parameter for an Options pointer.
|
||||
// RequiredAudience is used to require a specific Audience `aud` in the claims.
|
||||
// Defaults to empty string `""` and means all audiences are allowed.
|
||||
func WithRequiredAudience(opt string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.RequiredAudience = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithRequiredClaims sets the RequiredClaims parameter for an Options pointer.
|
||||
// RequiredClaims is used to require specific claims in the token
|
||||
// Defaults to empty map (nil) and won't check for anything else
|
||||
// Works with primitive types, slices and maps.
|
||||
// Please observe: slices and strings checks that the token contains it, but more is allowed.
|
||||
// Required claim []string{"bar"} matches token []string{"foo", "bar", "baz"}
|
||||
// Required claim map[string]string{{"foo": "bar"}} matches token map[string]string{{"a": "b"},{"foo": "bar"},{"c": "d"}}
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ```go
|
||||
// map[string]interface{}{
|
||||
// "foo": "bar",
|
||||
// "bar": 1337,
|
||||
// "baz": []string{"bar"},
|
||||
// "oof": []map[string]string{
|
||||
// {"bar": "baz"},
|
||||
// },
|
||||
// },
|
||||
// ```
|
||||
func WithRequiredClaims(opt map[string]interface{}) Option {
|
||||
return func(opts *Options) {
|
||||
opts.RequiredClaims = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithDisableKeyID sets the DisableKeyID parameter for an Options pointer.
|
||||
// DisableKeyID adjusts if a KeyID needs to be extracted from the token or not
|
||||
// Defaults to false and means KeyID is required to be present in both the jwks and token
|
||||
// The OIDC specification doesn't require KeyID if there's only one key in the jwks:
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#Signing
|
||||
//
|
||||
// This also means that if enabled, refresh of the jwks will be done if the token can't be
|
||||
// validated due to invalid key. The JWKS fetch will fail if there's more than one key present.
|
||||
func WithDisableKeyID(opt bool) Option {
|
||||
return func(opts *Options) {
|
||||
opts.DisableKeyID = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithHttpClient sets the HttpClient parameter for an Options pointer.
|
||||
// HttpClient takes a *http.Client for external calls
|
||||
// Defaults to http.DefaultClient
|
||||
func WithHttpClient(opt *http.Client) Option {
|
||||
return func(opts *Options) {
|
||||
opts.HttpClient = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithTokenString sets the TokenString parameter for an Options pointer.
|
||||
// TokenString makes it possible to configure how the JWT token should be extracted from
|
||||
// an http header. Not supported by Echo JWT and will be ignored if used by it.
|
||||
// Defaults to: 'Authorization: Bearer JWT'
|
||||
func WithTokenString(setters ...TokenStringOption) Option {
|
||||
var tokenString []TokenStringOption
|
||||
tokenString = append(tokenString, setters...)
|
||||
|
||||
return func(opts *Options) {
|
||||
opts.TokenString = append(opts.TokenString, tokenString)
|
||||
}
|
||||
}
|
||||
|
||||
// WithClaimsContextKeyName sets the ClaimsContextKeyName parameter for an Options pointer.
|
||||
// ClaimsContextKeyName is the name of key that will be used to pass claims using request context.
|
||||
// Not supported by Echo JWT and will be ignored if used by it.
|
||||
//
|
||||
// Important note: If you change this using `options.WithClaimsContextKeyName("foo")`, then
|
||||
// you also need to use it like this:
|
||||
// `claims, ok := r.Context().Value(options.ClaimsContextKeyName("foo")).(map[string]interface{})`
|
||||
//
|
||||
// Default: `options.DefaultClaimsContextKeyName`
|
||||
// Used like this: ``claims, ok := r.Context().Value(options.DefaultClaimsContextKeyName).(map[string]interface{})``
|
||||
//
|
||||
// When used with gin, it is converted to normal string - by default:
|
||||
// `claimsValue, found := c.Get("claims")`
|
||||
func WithClaimsContextKeyName(opt string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.ClaimsContextKeyName = ClaimsContextKeyName(opt)
|
||||
}
|
||||
}
|
||||
|
||||
// WithErrorHandler sets the ErrorHandler parameter for an Options pointer.
|
||||
// You can pass a function to run custom logic on errors, logging as an example.
|
||||
// Defaults to nil
|
||||
func WithErrorHandler(opt ErrorHandler) Option {
|
||||
return func(opts *Options) {
|
||||
opts.ErrorHandler = opt
|
||||
}
|
||||
}
|
101
options/options_test.go
Normal file
101
options/options_test.go
Normal file
@ -0,0 +1,101 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
expectedResult := &Options{
|
||||
Issuer: "foo",
|
||||
DiscoveryUri: "foo",
|
||||
JwksUri: "foo",
|
||||
JwksFetchTimeout: 1234 * time.Second,
|
||||
JwksRateLimit: 1234,
|
||||
FallbackSignatureAlgorithm: "foo",
|
||||
AllowedTokenDrift: 1234 * time.Second,
|
||||
LazyLoadJwks: true,
|
||||
RequiredTokenType: "foo",
|
||||
RequiredAudience: "foo",
|
||||
RequiredClaims: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
DisableKeyID: true,
|
||||
HttpClient: &http.Client{
|
||||
Timeout: 1234 * time.Second,
|
||||
},
|
||||
TokenString: nil,
|
||||
ClaimsContextKeyName: ClaimsContextKeyName("foo"),
|
||||
ErrorHandler: nil,
|
||||
}
|
||||
|
||||
expectedFirstTokenString := &TokenStringOptions{
|
||||
HeaderName: "foo",
|
||||
TokenPrefix: "bar_",
|
||||
ListSeparator: ",",
|
||||
}
|
||||
|
||||
expectedSecondTokenString := &TokenStringOptions{
|
||||
HeaderName: "too",
|
||||
TokenPrefix: "lar_",
|
||||
ListSeparator: "",
|
||||
}
|
||||
|
||||
setters := []Option{
|
||||
WithIssuer("foo"),
|
||||
WithDiscoveryUri("foo"),
|
||||
WithJwksUri("foo"),
|
||||
WithJwksFetchTimeout(1234 * time.Second),
|
||||
WithJwksRateLimit(1234),
|
||||
WithFallbackSignatureAlgorithm("foo"),
|
||||
WithAllowedTokenDrift(1234 * time.Second),
|
||||
WithLazyLoadJwks(true),
|
||||
WithRequiredTokenType("foo"),
|
||||
WithRequiredAudience("foo"),
|
||||
WithRequiredClaims(map[string]interface{}{
|
||||
"foo": "bar",
|
||||
}),
|
||||
WithDisableKeyID(true),
|
||||
WithHttpClient(&http.Client{
|
||||
Timeout: 1234 * time.Second,
|
||||
}),
|
||||
WithTokenString(
|
||||
WithTokenStringHeaderName("foo"),
|
||||
WithTokenStringTokenPrefix("bar_"),
|
||||
WithTokenStringListSeparator(","),
|
||||
),
|
||||
WithTokenString(
|
||||
WithTokenStringHeaderName("too"),
|
||||
WithTokenStringTokenPrefix("lar_"),
|
||||
),
|
||||
WithClaimsContextKeyName("foo"),
|
||||
WithErrorHandler(nil),
|
||||
}
|
||||
|
||||
result := &Options{}
|
||||
|
||||
for _, setter := range setters {
|
||||
setter(result)
|
||||
}
|
||||
|
||||
resultFirstTokenString := &TokenStringOptions{}
|
||||
resultSecondTokenString := &TokenStringOptions{}
|
||||
|
||||
for _, setter := range result.TokenString[0] {
|
||||
setter(resultFirstTokenString)
|
||||
}
|
||||
|
||||
for _, setter := range result.TokenString[1] {
|
||||
setter(resultSecondTokenString)
|
||||
}
|
||||
|
||||
// Needed or else expectedResult can't be compared to result
|
||||
result.TokenString = nil
|
||||
|
||||
require.Equal(t, expectedResult, result)
|
||||
require.Equal(t, expectedFirstTokenString, resultFirstTokenString)
|
||||
require.Equal(t, expectedSecondTokenString, resultSecondTokenString)
|
||||
}
|
69
options/tokenstring.go
Normal file
69
options/tokenstring.go
Normal file
@ -0,0 +1,69 @@
|
||||
package options
|
||||
|
||||
// TokenStringOptions handles the settings for how to extract the token from a request.
|
||||
type TokenStringOptions struct {
|
||||
HeaderName string
|
||||
TokenPrefix string
|
||||
ListSeparator string
|
||||
PostExtractionFn func(string) (string, error)
|
||||
}
|
||||
|
||||
// NewTokenString takes TokenStringOption setters and returns
|
||||
// a TokenStringOptions pointer.
|
||||
// Mainly used by the internal functions and most likely not
|
||||
// needed by any external application using this library.
|
||||
func NewTokenString(setters ...TokenStringOption) *TokenStringOptions {
|
||||
opts := &TokenStringOptions{
|
||||
HeaderName: "Authorization",
|
||||
TokenPrefix: "Bearer ",
|
||||
ListSeparator: "",
|
||||
PostExtractionFn: nil,
|
||||
}
|
||||
|
||||
for _, setter := range setters {
|
||||
setter(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// TokenStringOption returns a function that modifies a TokenStringOptions pointer.
|
||||
type TokenStringOption func(*TokenStringOptions)
|
||||
|
||||
// WithTokenStringHeaderName sets the HeaderName parameter for a TokenStringOptions pointer.
|
||||
// HeaderName is the name of the header.
|
||||
// Default: "Authorization"
|
||||
func WithTokenStringHeaderName(opt string) TokenStringOption {
|
||||
return func(opts *TokenStringOptions) {
|
||||
opts.HeaderName = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithTokenStringTokenPrefix sets the TokenPrefix parameter for a TokenStringOptions pointer.
|
||||
// TokenPrefix defines the prefix that should be trimmed from the header value
|
||||
// to extract the token.
|
||||
// Default: "Bearer "
|
||||
func WithTokenStringTokenPrefix(opt string) TokenStringOption {
|
||||
return func(opts *TokenStringOptions) {
|
||||
opts.TokenPrefix = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithTokenStringListSeparator sets the ListSeparator parameter for a TokenStringOptions pointer.
|
||||
// ListSeparator defines if the value of the header is a list or not.
|
||||
// The value will be split (up to 20 slices) by the ListSeparator.
|
||||
// Default disabled: ""
|
||||
func WithTokenStringListSeparator(opt string) TokenStringOption {
|
||||
return func(opts *TokenStringOptions) {
|
||||
opts.ListSeparator = opt
|
||||
}
|
||||
}
|
||||
|
||||
// WithTokenStringPostExtractionFn sets the PostExtractionFn parameter for a TokenStringOptions pointer.
|
||||
// PostExtractionFn will be run if not nil after a token has been successfully extracted.
|
||||
// Default: nil
|
||||
func WithTokenStringPostExtractionFn(opt func(string) (string, error)) TokenStringOption {
|
||||
return func(opts *TokenStringOptions) {
|
||||
opts.PostExtractionFn = opt
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user