GraphQL API and utilities for the rpdata project
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

198 lines
4.8 KiB

package auth
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"
jwt "github.com/dgrijalva/jwt-go"
)
var contextKey = &struct{ data string }{"Token Context Key"}
// ErrNoKid is returned if the key id is missing from the jwt token header,
var ErrNoKid = errors.New("Missing \"kid\" field in token")
// ErrKeyNotFound is returned if the key wasn't found.
var ErrKeyNotFound = errors.New("Key not found")
// ErrInvalidClaims is returned by parseClaims if the claims cannot be parsed
var ErrInvalidClaims = errors.New("Invalid claims in token")
// ErrExpired is returned by parseClaims if the expiry date is in the past
var ErrExpired = errors.New("Claims have already expired")
// ErrWrongUser is returned by CheckToken if the key cannot represent this user
var ErrWrongUser = errors.New("Key is not valid for this user")
// ErrWrongPermissions is returned by CheckToken if the key cannot claim one or more of its permissions
var ErrWrongPermissions = errors.New("Key is not valid for this user")
// ErrDeletedUser is returned by CheckToken if the key can represent this user, but the user doesn't exist.
var ErrDeletedUser = errors.New("User was not found")
// A Token contains the parsed results from an bearer token. Its methods are safe to use with a nil receiver, but
// the userID should be checked.
type Token struct {
UserID string
Permissions []string
}
// Authenticated returns true if the token is non-nil and parsed
func (token *Token) Authenticated() bool {
return token != nil && token.UserID != ""
}
// Permitted returns true if the token is non-nil and has the given permission or the "admin" permission
func (token *Token) Permitted(permissions ...string) bool {
if token == nil {
return false
}
for _, tokenPermission := range token.Permissions {
if tokenPermission == "admin" {
return true
}
for _, permission := range permissions {
if permission == tokenPermission {
return true
}
}
}
return false
}
// PermittedUser checks the first permission if the user matches, the second otherwise. This is a common
// pattern.
func (token *Token) PermittedUser(userID, permissionIfUser, permissionOtherwise string) bool {
if token == nil {
return false
}
if token.UserID == userID {
return token.Permitted(permissionIfUser)
}
return token.Permitted(permissionOtherwise)
}
// TokenFromContext gets the token from context.
func TokenFromContext(ctx context.Context) *Token {
token, ok := ctx.Value(contextKey).(*Token)
if !ok {
return nil
}
return token
}
// RequestWithToken either returns the request, or the request with a new context that
// has the token.
func RequestWithToken(r *http.Request) *http.Request {
header := r.Header.Get("Authorization")
if header == "" {
return r
}
if !strings.HasPrefix(header, "Bearer ") {
return r
}
token, err := CheckToken(header[7:])
if err != nil {
return r
}
return r.WithContext(context.WithValue(r.Context(), contextKey, &token))
}
// CheckToken reads the token string and returns a token if everything is kosher.
func CheckToken(tokenString string) (token Token, err error) {
var key Key
jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) {
if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", jwtToken.Header["alg"])
}
kid, ok := jwtToken.Header["kid"].(string)
if !ok {
return nil, ErrNoKid
}
key, err = FindKey(kid)
if err != nil || key.ID == "" {
return nil, ErrKeyNotFound
}
return []byte(key.Secret), nil
})
if err != nil {
return Token{}, err
}
userid, permissions, err := parseClaims(jwtToken.Claims)
if err != nil {
return Token{}, err
}
if !key.ValidForUser(userid) {
return Token{}, ErrWrongUser
}
user, err := FindUser(userid)
if err != nil {
return Token{}, ErrDeletedUser
}
for _, permission := range permissions {
found := false
for _, userPermission := range user.Permissions {
if permission == userPermission {
found = true
break
}
}
if !found {
return Token{}, ErrWrongPermissions
}
}
return Token{UserID: token.UserID, Permissions: permissions}, nil
}
func parseClaims(jwtClaims jwt.Claims) (userid string, permissions []string, err error) {
mapClaims, ok := jwtClaims.(jwt.MapClaims)
if !ok {
return "", nil, ErrInvalidClaims
}
if !mapClaims.VerifyExpiresAt(time.Now().Unix(), true) {
return "", nil, ErrExpired
}
if userid, ok = mapClaims["user"].(string); !ok {
return "", nil, ErrInvalidClaims
}
if claimedPermissions, ok := mapClaims["permissions"].([]interface{}); ok {
for _, permission := range claimedPermissions {
if permission, ok := permission.(string); ok {
permissions = append(permissions, permission)
}
}
}
if len(permissions) == 0 {
return "", nil, ErrInvalidClaims
}
return
}