Last active
March 12, 2016 20:01
-
-
Save abourget/444318d4f58f38c93460 to your computer and use it in GitHub Desktop.
Sample security middleware for goa's Security framework..
This file contains 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 characters
... | |
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("...") | |
} |
This file contains 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 characters
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 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 { | |
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 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment