* Remove error when tokens mismatch from public id but log returned errors from repo.
187 lines
5.7 KiB
Go
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]
|
|
}
|