Files
threshold/internal/servers/controller/handlers/authtoken_intercept.go
T
Todd Knight 2ce9affeea Don't use repo to check impossible token requests and repo doesn't return error on token value mismatch. (#171)
* Remove error when tokens mismatch from public id but log returned errors from repo.
2020-07-08 09:09:09 -07:00

187 lines
5.7 KiB
Go

package handlers
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/watchtower/internal/servers/controller/common"
"google.golang.org/grpc/metadata"
)
const (
headerAuthMethod = "Authorization"
httpOnlyCookieName = "wt-http-token-cookie"
jsVisibleCookieName = "wt-js-token-cookie"
)
// TokenAuthenticator returns a function that can be used in grpc-gateway's runtime.WithMetadata ServerOption.
// It looks at the cookies and headers of the incoming request and returns metadata that can later be
// used by handlers to build a TokenMetadata using the ToTokenMetadata function.
func TokenAuthenticator(l hclog.Logger, tokenRepo common.AuthTokenRepoFactory) func(context.Context, *http.Request) metadata.MD {
return func(ctx context.Context, req *http.Request) metadata.MD {
tMD := TokenMetadata{}
if authHeader := req.Header.Get(headerAuthMethod); authHeader != "" {
headerSplit := strings.SplitN(strings.TrimSpace(authHeader), " ", 2)
if len(headerSplit) == 2 && strings.EqualFold(strings.TrimSpace(headerSplit[0]), "bearer") {
tMD.recievedTokenType = authTokenTypeBearer
tMD.bearerPayload = strings.TrimSpace(headerSplit[1])
}
}
if tMD.recievedTokenType != authTokenTypeBearer {
if hc, err := req.Cookie(httpOnlyCookieName); err == nil {
tMD.httpCookiePayload = hc.Value
}
if jc, err := req.Cookie(jsVisibleCookieName); err == nil {
tMD.jsCookiePayload = jc.Value
}
if tMD.httpCookiePayload != "" && tMD.jsCookiePayload != "" {
tMD.recievedTokenType = authTokenTypeSplitCookie
}
}
if tMD.recievedTokenType == authTokenTypeUnknown || tMD.token() == "" || tMD.publicId() == "" {
return tMD.toMetadata()
}
repo, err := tokenRepo()
if err != nil {
l.Error("failed to get authtoken repo", "error", err)
return tMD.toMetadata()
}
at, err := repo.ValidateToken(ctx, tMD.publicId(), tMD.token())
if err != nil {
l.Error("failed to validate token", "error", err)
}
if at != nil {
tMD.UserId = at.GetIamUserId()
}
return tMD.toMetadata()
}
}
type tokenFormat int
const (
authTokenTypeUnknown tokenFormat = iota
authTokenTypeBearer
authTokenTypeSplitCookie
)
// TokenMetadata allows easy writing/reading of tokens to clients and authenticating the provided token.
// Expected usage for authorization is
// func (s *Service) GetResource(ctx context.Context, req GetResourceRequest) (GetResourceResponse, error) {
// amd := handlers.ToTokenMetadata(ctx)
// if !authorizer.isAuthorized(amd.UserId, "ReadResource", req.GetId()) { return nil, UnauthorizedError }
// ...
//
// A new token will be created by the Authenticate method on an Organization. The token value will be returned
// through json and not be intercepted by these tools.
// TODO: Intercept the outgoing Authenticate/Deauthenticate response and manipulate
// the response if the token type was cookie.
type TokenMetadata struct {
// Only set the UserId if the token was found and was not expired.
UserId string
recievedTokenType tokenFormat
bearerPayload string
jsCookiePayload string
httpCookiePayload string
}
const (
mdAuthTokenUserKey = "wt-authtoken-user-key"
mdAuthTokenBearerTokenKey = "wt-authtoken-bearer-token-key"
mdAuthTokenHttpTokenKey = "wt-authtoken-http-token-key"
mdAuthTokenJsTokenKey = "wt-authtoken-js-token-key"
mdAuthTokenTypeKey = "wt-authtoken-type-key"
)
// ToTokenMetadata takes an incoming context and builds a TokenMetadata based on the metadata attached to it.
// If the context has no TokenMetadata attached to it an empty TokenMetadata is returned.
func ToTokenMetadata(ctx context.Context) TokenMetadata {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return TokenMetadata{}
}
tMD := TokenMetadata{}
if uid := md.Get(mdAuthTokenUserKey); len(uid) > 0 {
tMD.UserId = uid[0]
}
if token := md.Get(mdAuthTokenBearerTokenKey); len(token) > 0 {
tMD.bearerPayload = token[0]
}
if token := md.Get(mdAuthTokenHttpTokenKey); len(token) > 0 {
tMD.httpCookiePayload = token[0]
}
if token := md.Get(mdAuthTokenJsTokenKey); len(token) > 0 {
tMD.jsCookiePayload = token[0]
}
if sType := md.Get(mdAuthTokenTypeKey); len(sType) > 0 {
if st, err := strconv.Atoi(sType[0]); err == nil {
tMD.recievedTokenType = tokenFormat(st)
}
}
return tMD
}
func (s TokenMetadata) toMetadata() metadata.MD {
md := metadata.MD{}
if s.UserId != "" {
md.Set(mdAuthTokenUserKey, s.UserId)
}
if s.bearerPayload != "" {
md.Set(mdAuthTokenBearerTokenKey, s.bearerPayload)
}
if s.httpCookiePayload != "" {
md.Set(mdAuthTokenHttpTokenKey, s.httpCookiePayload)
}
if s.jsCookiePayload != "" {
md.Set(mdAuthTokenJsTokenKey, s.jsCookiePayload)
}
if s.recievedTokenType != authTokenTypeUnknown {
md.Set(mdAuthTokenTypeKey, fmt.Sprint(s.recievedTokenType))
}
return md
}
// publicId returns the public id parsed out of the provided auth token. If the provided auth token
// is malformed then this returns an empty string.
func (s TokenMetadata) publicId() string {
tok := ""
switch s.recievedTokenType {
case authTokenTypeBearer:
tok = s.bearerPayload
case authTokenTypeSplitCookie:
tok = s.jsCookiePayload + s.httpCookiePayload
}
l := strings.Split(tok, "_")[:strings.Count(tok, "_")]
if len(l) != 2 {
return ""
}
return strings.Join(l, "_")
}
// token returns the token value parsed out of the provided auth token. If the provided auth token
// is malformed then this returns an empty string.
func (s TokenMetadata) token() string {
var tok string
switch s.recievedTokenType {
case authTokenTypeBearer:
tok = s.bearerPayload
case authTokenTypeSplitCookie:
tok = s.jsCookiePayload + s.httpCookiePayload
}
l := strings.Split(tok, "_")
if len(l) != 3 {
return ""
}
return l[2]
}