From 83f90c1bbd72cc24c2c88f5da4454f5d29f0e703 Mon Sep 17 00:00:00 2001 From: Vikram Rangnekar Date: Thu, 28 Mar 2019 22:34:42 -0400 Subject: [PATCH] Add Auth0 JWT support --- .dockerignore | 1 + README.md | 40 +++++++++++++++++++++++++++++++++++++--- dev.yml | 3 ++- go.mod | 2 +- prod.yml | 3 ++- serv/auth.go | 5 +++-- serv/auth_jwt.go | 24 +++++++++++++++++++++++- serv/http.go | 15 ++++++++++++--- 8 files changed, 81 insertions(+), 12 deletions(-) diff --git a/.dockerignore b/.dockerignore index 9e0b390..3ef819f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,4 @@ example tmp *.md web/build +docker-compose.* diff --git a/README.md b/README.md index 143f2f3..2aeb115 100644 --- a/README.md +++ b/README.md @@ -234,7 +234,7 @@ auth: # type: jwt # cookie: _app_session # secret: abc335bfcfdb04e50db5bb0a4d67ab9 -# public_key_file: abc335bfcfdb04e50db5bb0a4d67ab9 +# public_key_file: /secrets/public_key.pem # public_key_type: ecdsa #rsa database: @@ -283,10 +283,44 @@ SG_AUTH_URL SG_AUTH_PASSWORD ``` +## Authentication + +You can only have one type of authentication enabled. You can either pick Rails or JWT. Uncomment the one you use and leave the rest commented out. + +#### JWT Tokens + +```yaml +auth: + type: jwt + provider: auth0 #none + cookie: _app_session + secret: abc335bfcfdb04e50db5bb0a4d67ab9 + public_key_file: /secrets/public_key.pem + public_key_type: ecdsa #rsa +``` + +For JWT tokens we currently support tokens from a provider like Auth0 +or if you have a custom solution then we look for the `user_id` in the +`subject` claim of of the `id token`. If you pick Auth0 then we derive two variables from the token `user_id` and `user_id_provider` for to use in your filters. + +We can get the JWT token either from the `authorization` header where we expect it to be a `bearer` token or if `cookie` is specified then we look there. + +For verified either a `secret` or a public key (ecdsa or rsa) is required. When using public keys they have to be in a PEM format file. + ## Deployment -How do I deploy the Super Graph service with my existing rails app? You have several options here. Esentially you need to ensure your app's session cookie -will be passed to this service. +How do I deploy the Super Graph service with my existing rails app? You have several options here. Esentially you need to ensure your app's session cookie will be passed to this service. + +#### Custom Docker Image + +Create a `Dockerfile` like the one below to roll your own +custom Super Graph docker image. And to build it `docker build -t my-super-graph .` + +```dockerfile +FROM dosco/super-graph:latest +WORKDIR /app +COPY *.yml ./ +``` #### Deploy under a subdomain For this to work you have to ensure that the option `:domain => :all` is added to your rails app config `Application.config.session_store` this will cause your rails app to create session cookies that can be shared with sub-domains. More info here diff --git a/dev.yml b/dev.yml index 9075ee9..ec7906c 100644 --- a/dev.yml +++ b/dev.yml @@ -50,9 +50,10 @@ auth: # auth: # type: jwt +# provider: auth0 # cookie: _app_session # secret: abc335bfcfdb04e50db5bb0a4d67ab9 -# public_key_file: abc335bfcfdb04e50db5bb0a4d67ab9 +# public_key_file: /secrets/public_key.pem # public_key_type: ecdsa #rsa database: diff --git a/go.mod b/go.mod index 23e8007..1fa38ab 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/dosco/super-graph require ( - github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3 + github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3 github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/garyburd/redigo v1.6.0 diff --git a/prod.yml b/prod.yml index a33d5e3..6fe97e0 100644 --- a/prod.yml +++ b/prod.yml @@ -43,9 +43,10 @@ auth: # auth: # type: jwt +# provider: auth0 # cookie: _app_session # secret: abc335bfcfdb04e50db5bb0a4d67ab9 -# public_key_file: abc335bfcfdb04e50db5bb0a4d67ab9 +# public_key_file: /secrets/public_key.pem # public_key_type: ecdsa #rsa database: diff --git a/serv/auth.go b/serv/auth.go index 0021a22..e388d92 100644 --- a/serv/auth.go +++ b/serv/auth.go @@ -15,8 +15,9 @@ const ( ) var ( - userIDKey = struct{}{} - errSessionData = errors.New("error decoding session data") + userIDProviderKey = struct{}{} + userIDKey = struct{}{} + errSessionData = errors.New("error decoding session data") ) func headerHandler(next http.HandlerFunc) http.HandlerFunc { diff --git a/serv/auth_jwt.go b/serv/auth_jwt.go index fe92590..fa0be92 100644 --- a/serv/auth_jwt.go +++ b/serv/auth_jwt.go @@ -4,15 +4,27 @@ import ( "context" "io/ioutil" "net/http" + "strings" jwt "github.com/dgrijalva/jwt-go" ) +const ( + jwtBase int = iota + jwtAuth0 +) + func jwtHandler(next http.HandlerFunc) http.HandlerFunc { var key interface{} + var jwtProvider int cookie := conf.GetString("auth.cookie") + provider := conf.GetString("auth.provider") + if provider == "auth0" { + jwtProvider = jwtAuth0 + } + conf.BindEnv("auth.secret", "SG_AUTH_SECRET") secret := conf.GetString("auth.secret") @@ -75,7 +87,17 @@ func jwtHandler(next http.HandlerFunc) http.HandlerFunc { } if claims, ok := token.Claims.(*jwt.StandardClaims); ok { - ctx := context.WithValue(r.Context(), userIDKey, claims.Id) + ctx := r.Context() + + if jwtProvider == jwtAuth0 { + sub := strings.Split(claims.Subject, "|") + if len(sub) != 2 { + ctx = context.WithValue(ctx, userIDProviderKey, sub[0]) + ctx = context.WithValue(ctx, userIDKey, sub[1]) + } + } else { + ctx = context.WithValue(ctx, userIDKey, claims.Subject) + } next.ServeHTTP(w, r.WithContext(ctx)) } diff --git a/serv/http.go b/serv/http.go index 46cf213..4fa773e 100644 --- a/serv/http.go +++ b/serv/http.go @@ -153,15 +153,24 @@ func authCheck(ctx context.Context) bool { } func varValues(ctx context.Context) map[string]interface{} { - userIDFn := fasttemplate.TagFunc(func(w io.Writer, _ string) (int, error) { + uidFn := fasttemplate.TagFunc(func(w io.Writer, _ string) (int, error) { if v := ctx.Value(userIDKey); v != nil { return w.Write([]byte(v.(string))) } return 0, errNoUserID }) + uidpFn := fasttemplate.TagFunc(func(w io.Writer, _ string) (int, error) { + if v := ctx.Value(userIDProviderKey); v != nil { + return w.Write([]byte(v.(string))) + } + return 0, errNoUserID + }) + return map[string]interface{}{ - "USER_ID": userIDFn, - "user_id": userIDFn, + "USER_ID": uidFn, + "user_id": uidFn, + "USER_ID_PROVIDER": uidpFn, + "user_id_provider": uidpFn, } }