Authorization works (yay)

The journal page once again loads as it should; now to migrate the remaining routes
This commit is contained in:
Daniel J. Summers 2018-03-31 19:58:44 -05:00
parent 9637b38a3f
commit 419c181eff
3 changed files with 97 additions and 35 deletions

View File

@ -26,10 +26,9 @@ type Request struct {
// properties that may be filled for history and notes. // properties that may be filled for history and notes.
type JournalRequest struct { type JournalRequest struct {
RequestID string `json:"requestId"` RequestID string `json:"requestId"`
UserID string `json:"userId"`
Text string `json:"text"` Text string `json:"text"`
AsOf int64 `json:"asOf"` AsOf int64 `json:"asOf"`
LastStatus string `json:"lastStatus"` LastStatus string `json:"lastStatus"`
History []History `json:"history"` History []History `json:"history,omitempty"`
Notes []Note `json:"notes"` Notes []Note `json:"notes,omitempty"`
} }

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/danieljsummers/myPrayerJournal/src/api/data" "github.com/danieljsummers/myPrayerJournal/src/api/data"
jwt "github.com/dgrijalva/jwt-go"
) )
/* Support */ /* Support */
@ -24,26 +25,33 @@ func sendError(w http.ResponseWriter, r *http.Request, err error) {
func sendJSON(w http.ResponseWriter, r *http.Request, result interface{}) { func sendJSON(w http.ResponseWriter, r *http.Request, result interface{}) {
w.Header().Set("Content-Type", "application/json; encoding=UTF-8") w.Header().Set("Content-Type", "application/json; encoding=UTF-8")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(map[string]interface{}{"data": result}); err != nil { if err := json.NewEncoder(w).Encode(result); err != nil {
sendError(w, r, err) sendError(w, r, err)
} }
} }
// userID is a convenience function to extract the subscriber ID from the user's JWT.
// NOTE: Do not call this from public routes; there are a lot of type assertions that won't be true if the request
// hasn't gone through the authorization process.
func userID(r *http.Request) string {
return r.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims)["sub"].(string)
}
/* Handlers */ /* Handlers */
// GET: /api/journal
func journal(w http.ResponseWriter, r *http.Request) { func journal(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(ContextUserKey) reqs := data.Journal(userID(r))
reqs := data.Journal(user.(string))
if reqs == nil { if reqs == nil {
reqs = []data.JournalRequest{} reqs = []data.JournalRequest{}
} }
sendJSON(w, r, reqs) sendJSON(w, r, reqs)
} }
// GET: /*
func staticFiles(w http.ResponseWriter, r *http.Request) { func staticFiles(w http.ResponseWriter, r *http.Request) {
// serve index for known routes handled client-side by the app // serve index for known routes handled client-side by the app
for _, prefix := range ClientPrefixes { for _, prefix := range ClientPrefixes {
log.Print("Checking " + r.URL.Path)
if strings.HasPrefix(r.URL.Path, prefix) { if strings.HasPrefix(r.URL.Path, prefix) {
w.Header().Add("Content-Type", "text/html") w.Header().Add("Content-Type", "text/html")
http.ServeFile(w, r, "./public/index.html") http.ServeFile(w, r, "./public/index.html")

View File

@ -1,13 +1,15 @@
package routes package routes
import ( import (
"context" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"github.com/auth0-community/go-auth0" "github.com/auth0/go-jwt-middleware"
jwt "github.com/dgrijalva/jwt-go"
"github.com/husobee/vestigo" "github.com/husobee/vestigo"
"gopkg.in/square/go-jose.v2"
) )
// AuthConfig contains the Auth0 configuration passed from the "auth" JSON object. // AuthConfig contains the Auth0 configuration passed from the "auth" JSON object.
@ -17,49 +19,102 @@ type AuthConfig struct {
ClientSecret string `json:"secret"` ClientSecret string `json:"secret"`
} }
// ContextKey is the type of key used in our contexts. // JWKS is a structure into which the JSON Web Key Set is unmarshaled.
type ContextKey string type JWKS struct {
Keys []JWK `json:"keys"`
}
// ContextUserKey is the key for the current user in the context. // JWK is a structure into which a single JSON Web Key is unmarshaled.
const ContextUserKey ContextKey = "user" type JWK 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"`
}
func withAuth(next http.HandlerFunc, cfg *AuthConfig) http.HandlerFunc { // authCfg is the Auth0 configuration provided at application startup.
return func(w http.ResponseWriter, r *http.Request) { var authCfg *AuthConfig
secret := []byte(cfg.ClientSecret)
secretProvider := auth0.NewKeyProvider(secret)
audience := []string{fmt.Sprintf("https://%s/userinfo", cfg.Domain)}
configuration := auth0.NewConfiguration(secretProvider, audience, fmt.Sprintf("https://%s/", cfg.Domain), jose.HS256) // jwksBytes is a cache of the JSON Web Key Set for this domain.
validator := auth0.NewValidator(configuration, nil) var jwksBytes = make([]byte, 0)
token, err := validator.ValidateRequest(r) // getPEMCert is a function to get the applicable certificate for a JSON Web Token.
func getPEMCert(token *jwt.Token) (string, error) {
cert := ""
if len(jwksBytes) == 0 {
resp, err := http.Get(fmt.Sprintf("https://%s/.well-known/jwks.json", authCfg.Domain))
if err != nil { if err != nil {
fmt.Println(err) return cert, err
fmt.Println("Token is not valid:", token) }
w.WriteHeader(http.StatusUnauthorized) defer resp.Body.Close()
w.Write([]byte("Unauthorized"))
} else { if jwksBytes, err = ioutil.ReadAll(resp.Body); err != nil {
values := make(map[string]interface{}) return cert, err
if err := token.Claims(secret, &values); err != nil {
sendError(w, r, err)
}
r = r.WithContext(context.WithValue(r.Context(), ContextUserKey, values["sub"]))
// TODO pass the user ID (sub) along; this -> doesn't work | r.Header.Add("user-id", token.Claims("sub"))
next(w, r)
} }
} }
jwks := JWKS{}
if err := json.Unmarshal(jwksBytes, &jwks); err != nil {
return cert, err
}
for k, v := range jwks.Keys[0].X5c {
if token.Header["kid"] == jwks.Keys[k].Kid {
cert = fmt.Sprintf("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----", v)
}
}
if cert == "" {
err := errors.New("unable to find appropriate key")
return cert, err
}
return cert, nil
}
// authZero is an instance of Auth0's JWT middlware. Since it doesn't support the http.HandlerFunc sig, it is wrapped
// below; it's defined outside that function, though, so it does not get recreated every time.
var authZero = jwtmiddleware.New(jwtmiddleware.Options{
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
if checkAud := token.Claims.(jwt.MapClaims).VerifyAudience(authCfg.ClientID, false); !checkAud {
return token, errors.New("invalid audience")
}
iss := fmt.Sprintf("https://%s/", authCfg.Domain)
if checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false); !checkIss {
return token, errors.New("invalid issuer")
}
cert, err := getPEMCert(token)
if err != nil {
panic(err.Error())
}
result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
return result, nil
},
SigningMethod: jwt.SigningMethodRS256,
})
// authMiddleware is a wrapper for the Auth0 middleware above with a signature Vestigo recognizes.
func authMiddleware(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := authZero.CheckJWT(w, r)
if err == nil {
f(w, r)
}
}
} }
// NewRouter returns a configured router to handle all incoming requests. // NewRouter returns a configured router to handle all incoming requests.
func NewRouter(cfg *AuthConfig) *vestigo.Router { func NewRouter(cfg *AuthConfig) *vestigo.Router {
authCfg = cfg
router := vestigo.NewRouter() router := vestigo.NewRouter()
for _, route := range routes { for _, route := range routes {
if route.IsPublic { if route.IsPublic {
router.Add(route.Method, route.Pattern, route.Func) router.Add(route.Method, route.Pattern, route.Func)
} else { } else {
router.Add(route.Method, route.Pattern, withAuth(route.Func, cfg)) router.Add(route.Method, route.Pattern, route.Func, authMiddleware)
} }
} }
return router return router