Last active
March 12, 2016 20:01
-
-
Save abourget/444318d4f58f38c93460 to your computer and use it in GitHub Desktop.
Revisions
-
abourget revised this gist
Mar 12, 2016 . 1 changed file with 14 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,14 @@ ... func serve() error { service := goa.New("Featurette") publicKeys := loadJWTPublicKeys(service) ... // JWTSecurity was generated, because I named my security method "jwt" app.JWTSecurity.Use(securityMiddleware(publicKeys)) return service.ListenAndServe("...") } -
abourget revised this gist
Mar 12, 2016 . 1 changed file with 27 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -14,6 +14,33 @@ import ( "golang.org/x/net/context" ) func loadJWTPublicKeys(service *goa.Service) (out []*rsa.PublicKey) { configs := []string{"JWT_PUBKEY1", "JWT_PUBKEY2", "JWT_PUBKEY3"} for _, configKey := range configs { pem := strings.Replace(viper.GetString(configKey), "\\n", "\n", -1) if pem == "" { continue } //fmt.Println("PEM:", pem) key, err := jwt.ParseRSAPublicKeyFromPEM([]byte(pem)) if err != nil { goa.Error(nil, fmt.Sprintf("error loading key %q: %s", configKey, err)) continue } service.Info("loaded PEM key", "env_var", fmt.Sprintf("FEATURETTE_%s", configKey)) out = append(out, key) } if len(out) == 0 { service.Error("couldn't load any signing JWT_PUBKEYs") os.Exit(1) } return } func securityMiddleware(publicKeys []*rsa.PublicKey) goa.Middleware { return func(h goa.Handler) goa.Handler { return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { -
abourget created this gist
Mar 12, 2016 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,114 @@ package main import ( "crypto/rsa" "encoding/json" "fmt" "net/http" "os" "strings" jwt "github.com/dgrijalva/jwt-go" "github.com/goadesign/goa" "github.com/spf13/viper" "golang.org/x/net/context" ) func securityMiddleware(publicKeys []*rsa.PublicKey) goa.Middleware { return func(h goa.Handler) goa.Handler { return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { method := goa.SecurityMethod(ctx).(*goa.APIKeySecurity) // optional check; you defined the design, you can assume it's // always "header". if method.In != "header" { return fmt.Errorf("whoops, method %q with in = %q not supported", method.Name, method.In) } val := req.Header.Get(method.Name) if val == "" { goa.Response(ctx).WriteHeader(401) return fmt.Errorf("missing header %q", method.Name) } if !strings.HasPrefix(strings.ToLower(val), "bearer ") { goa.Response(ctx).WriteHeader(401) return fmt.Errorf("invalid or malformed %q header, expected 'Authorization: Bearer JWT-token...'", val) } incomingToken := strings.Split(val, " ")[1] token, err := validateTokenWithKeys(incomingToken, publicKeys) if err != nil { goa.Info(ctx, "JWT token validation failed", "err", err) w := goa.Response(ctx) w.WriteHeader(401) json.NewEncoder(w).Encode(map[string]interface{}{ "error": "jwt_invalid", "message": "JWT validation failed", }) return nil } var claimedScopes = make(map[string]bool) if token.Claims["scopes"] != nil { scopes, _ := token.Claims["scopes"].(string) for _, scope := range strings.Split(scopes, ",") { claimedScopes[scope] = true } } requiredScopes := goa.Scopes(ctx) for _, scope := range requiredScopes { if !claimedScopes[scope] { goa.Info(ctx, "missing required scope in JWT token", "scope", scope) w := goa.Response(ctx) w.WriteHeader(401) json.NewEncoder(w).Encode(map[string]interface{}{ "error": "scope_not_present", "message": fmt.Sprintf("Required scope %q not present in JWT claims", scope), }) return nil } } return h(context.WithValue(ctx, jwtKey, token), rw, req) } } } // validateTokenWithKeys parses the JWT token with multiple keys, and returns // the first that is valid. This is to allow key rotation of signing authority // without disrupting current keys, and letting their expiry take effect. func validateTokenWithKeys(incomingToken string, keys []*rsa.PublicKey) (token *jwt.Token, err error) { for _, pubkey := range keys { token, err = jwt.Parse(incomingToken, func(token *jwt.Token) (interface{}, error) { if token.Method.Alg() != "RS256" { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } return pubkey, nil }) if err == nil { return } } return } const ( jwtKey contextKey = iota + 1 ) type contextKey int // JWT retrieves the JWT token from a `context` that went through our security // middleware. func JWT(ctx context.Context) *jwt.Token { token, ok := ctx.Value(jwtKey).(*jwt.Token) if !ok { return nil } return token }