Skip to content

Instantly share code, notes, and snippets.

@abourget
Last active March 12, 2016 20:01
Show Gist options
  • Select an option

  • Save abourget/444318d4f58f38c93460 to your computer and use it in GitHub Desktop.

Select an option

Save abourget/444318d4f58f38c93460 to your computer and use it in GitHub Desktop.

Revisions

  1. abourget revised this gist Mar 12, 2016. 1 changed file with 14 additions and 0 deletions.
    14 changes: 14 additions & 0 deletions main.go
    Original 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("...")
    }
  2. abourget revised this gist Mar 12, 2016. 1 changed file with 27 additions and 0 deletions.
    27 changes: 27 additions & 0 deletions sample_goa_security_middleware.go
    Original 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 {
  3. abourget created this gist Mar 12, 2016.
    114 changes: 114 additions & 0 deletions sample_goa_security_middleware.go
    Original 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
    }