Skip to content

Commit

Permalink
ORGANIC-467. Added Okta JWT parser "properly", mostly.
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasyip committed Nov 28, 2018
1 parent 5f0f6a4 commit 163a708
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 78 deletions.
177 changes: 120 additions & 57 deletions api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,88 @@ import (
"context"
"net/http"

jwt "github.com/dgrijalva/jwt-go"
"github.com/netlify/git-gateway/conf"
"github.com/sirupsen/logrus"
"github.com/okta/okta-jwt-verifier-golang"
)

type Authenticator interface {
// authenticate checks incoming requests for tokens presented using the Authorization header
authenticate(w http.ResponseWriter, r *http.Request) (context.Context, error)
getName() string
}

type Authorizer interface {
// authorize checks incoming requests for roles data in tokens that is parsed and verified by prior authentication step
authorize(w http.ResponseWriter, r *http.Request) (context.Context, error)
getName() string
}

type Auth struct {
config *conf.GlobalConfiguration
authenticator Authenticator
authorizer Authorizer
version string
}

type JWTAuthenticator struct {
name string
auth Auth
}

type OktaJWTAuthenticator struct {
name string
auth Auth
}

type RolesAuthorizer struct {
name string
auth Auth
}

func NewAuthWithVersion(ctx context.Context, globalConfig *conf.GlobalConfiguration, version string) *Auth {
auth := &Auth{config: globalConfig, version: version}

auth.authenticator = &OktaJWTAuthenticator{name: "bearer-jwt-token", auth: *auth}
auth.authorizer = &RolesAuthorizer{name: "bearer-jwt-token-roles", auth: *auth}

return auth
}

// check both authentication and authorization
func (a *Auth) accessControl(w http.ResponseWriter, r *http.Request) (context.Context, error) {
_, err := a.authenticate(w, r)
logrus.Infof("Authenticate with: %v", a.authenticator.getName())
ctx, err := a.authenticator.authenticate(w, r)
if err != nil {
return nil, err
}

return a.authorize(w, r)
logrus.Infof("Authorizing with: %v", a.authorizer.getName())
return a.authorizer.authorize(w, r.WithContext(ctx))
}

func (a *Auth) extractBearerToken(w http.ResponseWriter, r *http.Request) (string, error) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return "", unauthorizedError("This endpoint requires a Bearer token")
}

matches := bearerRegexp.FindStringSubmatch(authHeader)
if len(matches) != 2 {
return "", unauthorizedError("This endpoint requires a Bearer token")
}

return matches[1], nil
}

func (a *JWTAuthenticator) getName() string {
return a.name
}

// authenticate checks incoming requests for tokens presented using the Authorization header
func (a *Auth) authenticate(w http.ResponseWriter, r *http.Request) (context.Context, error) {
func (a *JWTAuthenticator) authenticate(w http.ResponseWriter, r *http.Request) (context.Context, error) {
logrus.Info("Getting auth token")
token, err := a.extractBearerToken(w, r)
token, err := a.auth.extractBearerToken(w, r)
if err != nil {
return nil, err
}
Expand All @@ -36,61 +94,36 @@ func (a *Auth) authenticate(w http.ResponseWriter, r *http.Request) (context.Con
return a.parseJWTClaims(token, r)
}

// authorize checks incoming requests for roles data in tokens that is parsed and verified by prior authentication step
func (a *Auth) authorize(w http.ResponseWriter, r *http.Request) (context.Context, error) {
ctx := r.Context()
claims := getClaims(ctx)
config := getConfig(ctx)

logrus.Infof("authenticate url: %v+", r.URL)
if claims == nil {
return nil, unauthorizedError("Access to endpoint not allowed: no claims found in Bearer token")
}

if len(config.Roles) == 0 {
return ctx, nil
}
func (a *JWTAuthenticator) parseJWTClaims(bearer string, r *http.Request) (context.Context, error) {
config := getConfig(r.Context())
p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
token, err := p.ParseWithClaims(bearer, &GatewayClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(config.JWT.Secret), nil
})

roles, ok := claims.AppMetaData["roles"]
if ok {
roleStrings, _ := roles.([]interface{})
for _, data := range roleStrings {
role, _ := data.(string)
for _, adminRole := range config.Roles {
if role == adminRole {
return ctx, nil
}
}
}
if err != nil {
return nil, unauthorizedError("Invalid token: %v", err)
}

return nil, unauthorizedError("Access to endpoint not allowed: your role doesn't allow access")
claims := token.Claims.(GatewayClaims)
return withClaims(r.Context(), &claims), nil
}

func NewAuthWithVersion(ctx context.Context, globalConfig *conf.GlobalConfiguration, version string) *Auth {
auth := &Auth{config: globalConfig, version: version}

return auth
func (a *OktaJWTAuthenticator) getName() string {
return a.name
}

func (a *Auth) extractBearerToken(w http.ResponseWriter, r *http.Request) (string, error) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return "", unauthorizedError("This endpoint requires a Bearer token")
}

matches := bearerRegexp.FindStringSubmatch(authHeader)
if len(matches) != 2 {
return "", unauthorizedError("This endpoint requires a Bearer token")
func (a *OktaJWTAuthenticator) authenticate(w http.ResponseWriter, r *http.Request) (context.Context, error) {
logrus.Info("Getting auth token")
token, err := a.auth.extractBearerToken(w, r)
if err != nil {
return nil, err
}

return matches[1], nil
logrus.Infof("Parsing JWT claims: %v", token)
return a.parseOktaJWTClaims(token, r)
}

func (a *Auth) parseJWTClaims(bearer string, r *http.Request) (context.Context, error) {
// Reimplemented to use Okta lib
// Original validation only work for HS256 algo,
// Okta supports RS256 only which requires public key downloading and caching (key rotation)
func (a *OktaJWTAuthenticator) parseOktaJWTClaims(bearer string, r *http.Request) (context.Context, error) {
config := getConfig(r.Context())

toValidate := map[string]string{}
Expand All @@ -106,17 +139,47 @@ func (a *Auth) parseJWTClaims(bearer string, r *http.Request) (context.Context,

_, err := verifier.VerifyAccessToken(bearer)

// @TODO? WARNING: Should be roles and other claims be checked here?

if err != nil {
return nil, unauthorizedError("Invalid token: %v", err)
}

claims := GatewayClaims{Email: "e", StandardClaims: jwt.StandardClaims{Audience: "a"}}

logrus.Infof("parseJWTClaims passed")
return withClaims(r.Context(), &claims), nil
}

// return nil, because the `github.go` is coded to send personal token
// both github oauth generates its own id, so oauth pass-thru is impossible
// we can improve the gateway to talk oauth with github.com, but we will
// still return nil here.
return nil, nil
func (a *RolesAuthorizer) getName() string {
return a.name
}

func (a *RolesAuthorizer) authorize(w http.ResponseWriter, r *http.Request) (context.Context, error) {
ctx := r.Context()
claims := getClaims(ctx)
config := getConfig(ctx)

logrus.Infof("authenticate url: %v+", r.URL)
logrus.Infof("claims: %v+", claims)
if claims == nil {
return nil, unauthorizedError("Access to endpoint not allowed: no claims found in Bearer token")
}

if len(config.Roles) == 0 {
return ctx, nil
}

roles, ok := claims.AppMetaData["roles"]
if ok {
roleStrings, _ := roles.([]interface{})
for _, data := range roleStrings {
role, _ := data.(string)
for _, adminRole := range config.Roles {
if role == adminRole {
return ctx, nil
}
}
}
}

return nil, unauthorizedError("Access to endpoint not allowed: your role doesn't allow access")
}
27 changes: 8 additions & 19 deletions api/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"net/url"

jwt "github.com/dgrijalva/jwt-go"
"github.com/netlify/git-gateway/conf"
"github.com/netlify/git-gateway/models"
)
Expand All @@ -20,8 +19,8 @@ func (c contextKey) String() string {
}

const (
accessTokenKey = contextKey("token")
tokenKey = contextKey("jwt")
accessTokenKey = contextKey("access_token")
tokenClaimsKey = contextKey("jwt_claims")
requestIDKey = contextKey("request_id")
configKey = contextKey("config")
instanceIDKey = contextKey("instance_id")
Expand All @@ -31,27 +30,17 @@ const (
netlifyIDKey = contextKey("netlify_id")
)

// withToken adds the JWT token to the context.
func withToken(ctx context.Context, token *jwt.Token) context.Context {
return context.WithValue(ctx, tokenKey, token)
}

// getToken reads the JWT token from the context.
func getToken(ctx context.Context) *jwt.Token {
obj := ctx.Value(tokenKey)
if obj == nil {
return nil
}

return obj.(*jwt.Token)
// withTokenClaims adds the JWT token claims to the context.
func withClaims(ctx context.Context, claims *GatewayClaims) context.Context {
return context.WithValue(ctx, tokenClaimsKey, claims)
}

func getClaims(ctx context.Context) *GatewayClaims {
token := getToken(ctx)
if token == nil {
claims := ctx.Value(tokenClaimsKey)
if claims == nil {
return nil
}
return token.Claims.(*GatewayClaims)
return claims.(*GatewayClaims)
}

func withRequestID(ctx context.Context, id string) context.Context {
Expand Down
2 changes: 0 additions & 2 deletions api/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ func (gh *GitHubGateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx = withProxyTarget(ctx, target)
ctx = withAccessToken(ctx, config.GitHub.AccessToken)

log := getLogEntry(r)
log.Infof("proxy.ServeHTTP: %+v\n", r.WithContext(ctx))
gh.proxy.ServeHTTP(w, r.WithContext(ctx))
}

Expand Down

0 comments on commit 163a708

Please sign in to comment.