Skip to content

Commit

Permalink
Add original things
Browse files Browse the repository at this point in the history
  • Loading branch information
Patrick Wang committed Aug 13, 2020
0 parents commit 9ceb9f7
Show file tree
Hide file tree
Showing 9 changed files with 1,041 additions and 0 deletions.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Robokache

The Q&A store for ROBOKOP.

Workflow:

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

## Getting started

### Install

```bash
>> go get -t ./...
```

### Run

```bash
>> go run ./cmd
```

* Got to <http://lvh.me:8080/>
* Copy ID token from developer tools into authentication field
* Have fun

### Test

```bash
>> openssl req -new -newkey rsa:1024 -days 365 -nodes -x509 -keyout test/certs/test.key -out test/certs/test.cert
>> go test ./test -coverpkg github.com/NCATS-Gamma/robokache/internal/robokache -coverprofile=cover.out
>> go tool cover -func=cover.out
```

## How it works

### Security

* Google Sign-in
* document visibility levels:
* private (1) - only the owner
* shareable (2) - anyone with the link
* public (3) - anyone
* visibility is assigned to both questions and answers
* the effective visibility of an answer is min(answer.visibility, question.visibility)
132 changes: 132 additions & 0 deletions api/openapi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
openapi: 3.0.3
info:
title: Robokache
description: Q&A store for ROBOKOP
version: 1.0.0
contact:
email: [email protected]
license:
name: MIT
url: https://opensource.org/licenses/MIT
security:
- google: []
paths:
/api/questions:
post:
summary: Create a question
requestBody:
content:
application/json:
schema:
type: string
responses:
'201':
description: Question created
content:
application/json:
schema:
type: string
'401':
$ref: '#/components/responses/UnauthorizedError'
get:
summary: Get questions
responses:
'200':
description: Questions
content:
application/json:
schema:
type: string
'401':
$ref: '#/components/responses/UnauthorizedError'
/api/questions/{id}:
get:
summary: Get question by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Question
content:
application/json:
schema:
type: string
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
$ref: '#/components/responses/NoSuchDocument'
/api/answers/{id}:
get:
summary: Get answer by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Answer
content:
application/json:
schema:
type: string
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
$ref: '#/components/responses/NoSuchDocument'
/api/questions/{question_id}/answers:
post:
summary: Create an answer
parameters:
- name: question_id
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: string
responses:
'201':
description: Answer created
content:
application/json:
schema:
type: string
'401':
$ref: '#/components/responses/UnauthorizedError'
get:
summary: Get answers by question ID
parameters:
- name: question_id
in: path
required: true
schema:
type: string
responses:
'200':
description: Answers
content:
application/json:
schema:
type: string
'401':
$ref: '#/components/responses/UnauthorizedError'
components:
securitySchemes:
google:
type: http
scheme: bearer
bearerFormat: jwt
reponses:
UnauthorizedError:
description: Access token is missing or invalid
NoSuchDocument:
description: Document ID not found
11 changes: 11 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package main

import "github.com/NCATS-Gamma/robokache/internal/robokache"

func main() {
robokache.SetupDB()

r := robokache.SetupRouter()
robokache.AddGUI(r)
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
90 changes: 90 additions & 0 deletions internal/robokache/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package robokache

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"

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

// HTTPClient implements Get()
type HTTPClient interface {
Get(url string) (*http.Response, error)
}

var (
// Client is used to get the authentication certificates
Client HTTPClient
)

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)
}

// GetUser verifies authorization and sets the userEmail context
func GetUser(c *gin.Context) {
// Get bearer (JWT) token from header
header := c.Request.Header
reqToken := header.Get("Authorization")
splitToken := strings.Split(reqToken, "Bearer ")
if len(splitToken) != 2 {
c.AbortWithStatusJSON(401, "No Authorization header provided")
return
}
reqToken = splitToken[1]

// 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")
fatal(err)
if resp.StatusCode != 200 {
return nil, errors.New("Failed to contact certification authority")
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
fatal(err)
var certs map[string]string
json.Unmarshal(body, &certs)
pem := certs[token.Header["kid"].(string)]
verifyKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(pem))
fatal(err)
return verifyKey, nil
})
if err != nil {
c.AbortWithStatusJSON(401, err.Error())
return
}

// Verify claims
claims, ok := token.Claims.(*jwt.MapClaims)
if !ok {
panic(errors.New("token.Claims -> *jwt.MapClaims assertion failed"))
}
if !token.Valid {
c.AbortWithStatusJSON(401, "INVALID iat/nbt/exp")
return
}
if !claims.VerifyAudience("297705140796-41v2ra13t7mm8uvu2dp554ov1btt80dg.apps.googleusercontent.com", true) {
c.AbortWithStatusJSON(401, fmt.Sprintf("INVALID aud: %s", (*claims)["aud"]))
return
}
if !issuedByGoogle(claims) {
c.AbortWithStatusJSON(401, fmt.Sprintf("INVALID iss: %s", (*claims)["iss"]))
return
}

// Return user email
c.Set("userEmail", (*claims)["email"].(string))
c.Next()
}
Loading

0 comments on commit 9ceb9f7

Please sign in to comment.