// New returns a middleware to be used with the JWTSecurity DSL definitions of goa. It supports the // scopes claim in the JWT and ensures goa-defined Security DSLs are properly validated. // // The steps taken by the middleware are: // // 1. Validate the "Bearer" token present in the "Authorization" header against the key(s) // given to New // 2. If scopes are defined in the design for the action validate them against the "scopes" JWT // claim // // The `exp` (expiration) and `nbf` (not before) date checks are validated by the JWT library. // // validationKeys can be one of these: // // * a single string // * a single []byte // * a list of string // * a list of []byte // * a single rsa.PublicKey // * a list of rsa.PublicKey // // The type of the keys determine the algorithms that will be used to do the check. The goal of // having lists of keys is to allow for key rotation, still check the previous keys until rotation // has been completed. // // You can define an optional function to do additional validations on the token once the signature // and the claims requirements are proven to be valid. Example: // // validationHandler, _ := goa.NewMiddleware(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { // token := jwt.ContextJWT(ctx) // if val, ok := token.Claims["is_uncle"].(string); !ok || val != "ben" { // return jwt.ErrJWTError("you are not uncle ben's") // } // }) // // Mount the middleware with the generated UseXX function where XX is the name of the scheme as // defined in the design, e.g.: // // app.UseJWT(jwt.New("secret", validationHandler, app.NewJWTSecurity())) // func New(validationKeys interface{}, validationFunc goa.Middleware, scheme *goa.JWTSecurity) goa.Middleware { var algo string var rsaKeys []*rsa.PublicKey var hmacKeys []string switch keys := validationKeys.(type) { case []*rsa.PublicKey: rsaKeys = keys algo = "RS" case *rsa.PublicKey: rsaKeys = []*rsa.PublicKey{keys} algo = "RS" case string: hmacKeys = []string{keys} algo = "HS" case []string: hmacKeys = keys algo = "HS" default: panic("invalid parameter to `jwt.New()`, only accepts *rsa.publicKey, []*rsa.PublicKey (for RSA-based algorithms) or a signing secret string (for HS algorithms)") } return func(nextHandler goa.Handler) goa.Handler { return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { // TODO: implement the QUERY string handler too if scheme.In != goa.LocHeader { return fmt.Errorf("whoops, security scheme with location (in) %q not supported", scheme.In) } val := req.Header.Get(scheme.Name) if val == "" { return ErrJWTError(fmt.Sprintf("missing header %q", scheme.Name)) } if !strings.HasPrefix(strings.ToLower(val), "bearer ") { return ErrJWTError(fmt.Sprintf("invalid or malformed %q header, expected 'Authorization: Bearer JWT-token...'", val)) } incomingToken := strings.Split(val, " ")[1] var token *jwt.Token var err error switch algo { case "RS": token, err = validateRSAKeys(rsaKeys, algo, incomingToken) case "HS": token, err = validateHMACKeys(hmacKeys, algo, incomingToken) default: panic("how did this happen ? unsupported algo in jwt middleware") } if err != nil { return ErrJWTError(fmt.Sprintf("JWT validation failed: %s", err)) } scopesInClaim, scopesInClaimList, err := parseClaimScopes(token) if err != nil { goa.LogError(ctx, err.Error()) return ErrJWTError(err) } requiredScopes := goa.ContextRequiredScopes(ctx) for _, scope := range requiredScopes { if !scopesInClaim[scope] { msg := "authorization failed: required 'scopes' not present in JWT claim" return ErrJWTError(msg, "required", requiredScopes, "scopes", scopesInClaimList) } } ctx = WithJWT(ctx, token) if validationFunc != nil { nextHandler = validationFunc(nextHandler) } return nextHandler(ctx, rw, req) } } }
// New returns a middleware to be used with the JWTSecurity DSL definitions of goa. It supports the // scopes claim in the JWT and ensures goa-defined Security DSLs are properly validated. // // The steps taken by the middleware are: // // 1. Validate the "Bearer" token present in the "Authorization" header against the key(s) // given to New // 2. If scopes are defined in the design for the action validate them against the "scopes" JWT // claim // // The `exp` (expiration) and `nbf` (not before) date checks are validated by the JWT library. // // validationKeys can be one of these: // // * a single []byte // * a single string // * a slice of []byte // * a slice of string // * a single *rsa.PublicKey // * a slice of *rsa.PublicKey // // Keys of type string or []byte are interepreted according to the signing method defined in the JWT // token (HMAC, RSA, etc.). // // You can define an optional function to do additional validations on the token once the signature // and the claims requirements are proven to be valid. Example: // // validationHandler, _ := goa.NewMiddleware(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { // token := jwt.ContextJWT(ctx) // if val, ok := token.Claims["is_uncle"].(string); !ok || val != "ben" { // return jwt.ErrJWTError("you are not uncle ben's") // } // }) // // Mount the middleware with the generated UseXX function where XX is the name of the scheme as // defined in the design, e.g.: // // jwtResolver, _ := jwt.NewSimpleResolver("secret") // app.UseJWT(jwt.New(jwtResolver, validationHandler, app.NewJWTSecurity())) // func New(resolver KeyResolver, validationFunc goa.Middleware, scheme *goa.JWTSecurity) goa.Middleware { return func(nextHandler goa.Handler) goa.Handler { return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { // TODO: implement the QUERY string handler too if scheme.In != goa.LocHeader { return fmt.Errorf("whoops, security scheme with location (in) %q not supported", scheme.In) } val := req.Header.Get(scheme.Name) if val == "" { return ErrJWTError(fmt.Sprintf("missing header %q", scheme.Name)) } if !strings.HasPrefix(strings.ToLower(val), "bearer ") { return ErrJWTError(fmt.Sprintf("invalid or malformed %q header, expected 'Authorization: Bearer JWT-token...'", val)) } incomingToken := strings.Split(val, " ")[1] var ( rsaKeys []*rsa.PublicKey hmacKeys [][]byte keys = resolver.SelectKeys(req) ) { for _, key := range keys { switch k := key.(type) { case *rsa.PublicKey: rsaKeys = append(rsaKeys, k) case []byte: hmacKeys = append(hmacKeys, k) case string: hmacKeys = append(hmacKeys, []byte(k)) } } } var ( token *jwt.Token err error validated = false ) if len(rsaKeys) > 0 { token, err = validateRSAKeys(rsaKeys, "RS", incomingToken) if err == nil { validated = true } } if !validated && len(hmacKeys) > 0 { token, err = validateHMACKeys(hmacKeys, "HS", incomingToken) if err == nil { validated = true } } if !validated { return ErrJWTError("JWT validation failed") } scopesInClaim, scopesInClaimList, err := parseClaimScopes(token) if err != nil { goa.LogError(ctx, err.Error()) return ErrJWTError(err) } requiredScopes := goa.ContextRequiredScopes(ctx) for _, scope := range requiredScopes { if !scopesInClaim[scope] { msg := "authorization failed: required 'scopes' not present in JWT claim" return ErrJWTError(msg, "required", requiredScopes, "scopes", scopesInClaimList) } } ctx = WithJWT(ctx, token) if validationFunc != nil { nextHandler = validationFunc(nextHandler) } return nextHandler(ctx, rw, req) } } }