Skip to content

Commit

Permalink
Implement Auth0 as user authentication (#52)
Browse files Browse the repository at this point in the history
* Change user authentication to use Auth0

* Update to Go version 1.17

The update and/or vscode plugins did some linting

* Fix up auth0 login via api docs page

* Change running port from 80 to 8080
  • Loading branch information
maximusunc authored Aug 25, 2021
1 parent 81ea312 commit 43c4e0a
Show file tree
Hide file tree
Showing 17 changed files with 709 additions and 517 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ data/

*.out
robokache.test

.vscode
.DS_Store
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Robokache

The Q&A store for ROBOKOP.
The Q&A store for qgraph.

Workflow:

1. authenticate via JWT (Google, Facebook, etc.)
1. authenticate via JWT (Google, GitHub, etc.)
2. push/get your files

## Getting started
Expand All @@ -31,7 +31,7 @@ Run the image:


```bash
>> docker run -it --name robokache -p 8080:80 robokache
>> docker run -it --name robokache -p 8080:8080 robokache
```

### Native
Expand All @@ -48,13 +48,14 @@ Run:
>> go run ./cmd
```

* Got to <http://lvh.me:8080/>
* Copy ID token from developer tools into authentication field
* Go to <http://localhost:8080/>
* Sign in via the buttons at the top of the page
* Copy ID token into authentication field
* Have fun

## Testing

Set up testing certificate (to emulate Google Auth):
Set up testing certificate:

```bash
>> openssl req -new -newkey rsa:1024 -days 365 -nodes -x509 -keyout test/certs/test.key -out test/certs/test.cert
Expand All @@ -71,7 +72,7 @@ Run tests and print coverage:

### Security

* Google Sign-in
* Auth0 Sign-in
* document visibility levels:
* private (1) - only the owner
* shareable (2) - anyone with the link
Expand Down
8 changes: 4 additions & 4 deletions api/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ openapi: 3.0.3

info:
title: Robokache
description: Large object document store for ROBOKOP
version: 4.1.4
description: Large object document store for qgraph
version: 4.1.5
contact:
email: [email protected]
license:
name: MIT
url: https://opensource.org/licenses/MIT
security:
- google: []
- bearerAuth: []
paths:
/api/document:
get:
Expand Down Expand Up @@ -239,7 +239,7 @@ components:
type: string
example: D8JnjJB5
securitySchemes:
google:
bearerAuth:
type: http
scheme: bearer
bearerFormat: jwt
Expand Down
2 changes: 1 addition & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ func main() {

r := robokache.SetupRouter()
robokache.AddGUI(r)
r.Run(":80") // listen and serve on 0.0.0.0:80 (for windows "localhost:80")
r.Run(":8080") // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
35 changes: 28 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
module github.com/NCATS-Gamma/robokache

go 1.14
go 1.17

require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.6.3
github.com/auth0/go-jwt-middleware v1.0.1
github.com/form3tech-oss/jwt-go v3.2.2+incompatible
github.com/gin-gonic/gin v1.7.4
github.com/jmoiron/sqlx v1.2.0
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-sqlite3 v1.14.2
github.com/sirupsen/logrus v1.6.0
github.com/sirupsen/logrus v1.8.1
github.com/speps/go-hashids v2.0.0+incompatible
github.com/stretchr/testify v1.4.0
github.com/stretchr/testify v1.7.0
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.9.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/json-iterator/go v1.1.11 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/ugorji/go/codec v1.2.6 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
golang.org/x/text v0.3.6 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
119 changes: 92 additions & 27 deletions go.sum

Large diffs are not rendered by default.

181 changes: 93 additions & 88 deletions internal/robokache/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,30 @@ package robokache
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"regexp"

"github.com/dgrijalva/jwt-go"
jwtmiddleware "github.com/auth0/go-jwt-middleware"
"github.com/form3tech-oss/jwt-go"
"github.com/gin-gonic/gin"
_ "github.com/mattn/go-sqlite3" // makes database/sql point to SQLite
)

type Response struct {
Message string `json:"message"`
}

type Jwks struct {
Keys []JSONWebKeys `json:"keys"`
}

type JSONWebKeys struct {
Kty string `json:"kty"`
Kid string `json:"kid"`
Use string `json:"use"`
N string `json:"n"`
E string `json:"e"`
X5c []string `json:"x5c"`
}

// HTTPClient implements Get()
type HTTPClient interface {
Get(url string) (*http.Response, error)
Expand All @@ -27,100 +41,91 @@ func init() {
Client = &http.Client{}
}

func issuedByGoogle(claims *jwt.MapClaims) bool {
return claims.VerifyIssuer("accounts.google.com", true) ||
claims.VerifyIssuer("https://accounts.google.com", true)
func validateUser(c *gin.Context) {
jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
// Verify 'aud' claim
audience := "https://qgraph.org/api"

// bug in form3tech-oss/jwt-go that doesn't accept list of audiences
// need to convert to list because auth0 sends one
// copied from https://github.com/leoromanovsky/golang-gin/pull/1/commits/eab87202b4a38471ee5744a879cd342a636d7990
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return token, errors.New("invalid claims type")
}

if audienceList, ok := claims["aud"].([]interface{}); ok {
auds := make([]string, len(audienceList))
for _, aud := range audienceList {
audStr, ok := aud.(string)
if !ok {
return token, errors.New("invalid audience type")
}
auds = append(auds, audStr)
}
claims["aud"] = auds
}

checkAudience := token.Claims.(jwt.MapClaims).VerifyAudience(audience, false)
if !checkAudience {
return token, errors.New("invalid audience")
}
// Verify 'iss' claim
iss := "https://qgraph.us.auth0.com/"
checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false)
if !checkIss {
return token, errors.New("invalid issuer")
}

cert, err := getPemCert(token)
if err != nil {
panic(err.Error())
}

// set user email for document permissions
userEmail := claims["https://qgraph.org/email"].(string)
c.Set("userEmail", &userEmail)

result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
return result, nil
},
// Requests don't need a JWT
CredentialsOptional: true,
SigningMethod: jwt.SigningMethodRS256,
})
if err := jwtMiddleware.CheckJWT(c.Writer, c.Request); err != nil {
c.AbortWithStatus(401)
}
c.Next()
}

// Gets bearer (JWT) token from header
// Only fails if the header is present and invalid
func GetRequestBearerToken(c *gin.Context) (string, error) {
matchBearer := regexp.MustCompile("^Bearer\\s([\\w.-]+)$")
func getPemCert(token *jwt.Token) (string, error) {
cert := ""
resp, err := Client.Get("https://qgraph.us.auth0.com/.well-known/jwks.json")

header := c.Request.Header
authorizationHeader := header.Get("Authorization")
if authorizationHeader == "" {
return "", nil
if err != nil {
return cert, err
}
defer resp.Body.Close()

bearer := matchBearer.FindStringSubmatch(authorizationHeader)
if bearer == nil {
return "", fmt.Errorf("Invalid Authorization header formatting")
}
var jwks = Jwks{}
err = json.NewDecoder(resp.Body).Decode(&jwks)

return bearer[1], nil
}

// Verifies authorization and sets the userEmail context
func GetUser(reqToken string) (*string, error) {
// Verify token authenticity
token, err := jwt.ParseWithClaims(reqToken, &jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
resp, err := Client.Get("https://www.googleapis.com/oauth2/v1/certs")
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, errors.New("Failed to contact certification authority")
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var certs map[string]string
json.Unmarshal(body, &certs)
pem := certs[token.Header["kid"].(string)]
verifyKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(pem))
if err != nil {
return nil, err
}
return verifyKey, nil
})
if err != nil {
return nil, err
return cert, err
}

// Verify claims
claims, ok := token.Claims.(*jwt.MapClaims)
if !ok {
return nil, errors.New("token.Claims -> *jwt.MapClaims assertion failed")
}
if !token.Valid {
return nil, errors.New("INVALID iat/nbt/exp")
}
if !claims.VerifyAudience("297705140796-41v2ra13t7mm8uvu2dp554ov1btt80dg.apps.googleusercontent.com", true) {
return nil, fmt.Errorf("INVALID aud: %s", (*claims)["aud"])
}
if !issuedByGoogle(claims) {
return nil, fmt.Errorf("INVALID iss: %s", (*claims)["iss"])
for k := range jwks.Keys {
if token.Header["kid"] == jwks.Keys[k].Kid {
cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
}
}

userEmail := (*claims)["email"].(string)

return &userEmail, nil
}

// Runs GetUser and GetRequestBearerToken and puts the results
// in the Gin context.
func AddUserToContext(c *gin.Context) {
reqToken, err := GetRequestBearerToken(c)
if err != nil {
handleErr(c, fmt.Errorf("Unauthorized: %v", err))
c.Abort()
return
}
if reqToken == "" {
c.Next()
return
}
userEmail, err := GetUser(reqToken)
if err != nil {
handleErr(c, fmt.Errorf("Unauthorized: %v", err))
c.Abort()
return
if cert == "" {
err := errors.New("unable to find appropriate key")
return cert, err
}

// Set user email on context and continue middleware chain
c.Set("userEmail", userEmail)
c.Next()
return cert, nil
}
Loading

0 comments on commit 43c4e0a

Please sign in to comment.