263 lines
7.8 KiB
Go
263 lines
7.8 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, iamRepo common.IamRepoFactory) func(context.Context, *http.Request) metadata.MD {
|
|
return func(ctx context.Context, req *http.Request) metadata.MD {
|
|
return TokenMetadata{}.toMetadata()
|
|
/*
|
|
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.receivedTokenType = authTokenTypeBearer
|
|
tMD.bearerPayload = strings.TrimSpace(headerSplit[1])
|
|
}
|
|
}
|
|
if tMD.receivedTokenType != 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.receivedTokenType = authTokenTypeSplitCookie
|
|
}
|
|
}
|
|
|
|
if tMD.receivedTokenType == authTokenTypeUnknown || tMD.token() == "" || tMD.publicId() == "" {
|
|
return tMD.toMetadata()
|
|
}
|
|
|
|
tokenRepo, err := tokenRepo()
|
|
if err != nil {
|
|
l.Error("failed to get authtoken repo", "error", err)
|
|
return tMD.toMetadata()
|
|
}
|
|
|
|
at, err := tokenRepo.ValidateToken(ctx, tMD.publicId(), tMD.token())
|
|
if err != nil {
|
|
l.Error("failed to validate token", "error", err)
|
|
}
|
|
if at != nil {
|
|
tMD.UserId = at.GetIamUserId()
|
|
} else {
|
|
tMD.UserId = "u_anon"
|
|
}
|
|
|
|
iamRepo, err := iamRepo()
|
|
if err != nil {
|
|
l.Error("failed to get iam repo", "error", err)
|
|
return tMD.toMetadata()
|
|
}
|
|
grantPairs, err := iamRepo.GrantsForUser(ctx, tMD.UserId)
|
|
if err != nil {
|
|
l.Error("failed to query for user grants", "error", err)
|
|
return tMD.toMetadata()
|
|
}
|
|
parsedGrants := make([]perms.Grant, 0, len(grantPairs))
|
|
for _, pair := range grantPairs {
|
|
parsed, err := perms.Parse(pair.ScopeId, tMD.UserId, pair.Grant)
|
|
if err != nil {
|
|
l.Error("failed to parse grant", "grant", pair.Grant, "error", err)
|
|
return tMD.toMetadata()
|
|
}
|
|
parsedGrants = append(parsedGrants, parsed)
|
|
}
|
|
|
|
var scopeId, id, pin string
|
|
var typ resource.Type
|
|
var act action.Type
|
|
typRaw := ctx.Value(globals.ContextTypeValue)
|
|
if typRaw != nil {
|
|
typ = typRaw.(resource.Type)
|
|
}
|
|
scopeIdRaw := ctx.Value(globals.ContextScopeValue)
|
|
if scopeIdRaw != nil {
|
|
scopeId = scopeIdRaw.(string)
|
|
}
|
|
actRaw := ctx.Value(globals.ContextActionValue)
|
|
if actRaw != nil {
|
|
act = actRaw.(action.Type)
|
|
}
|
|
idRaw := ctx.Value(globals.ContextResourceValue)
|
|
if idRaw != nil {
|
|
id = idRaw.(string)
|
|
}
|
|
pinRaw := ctx.Value(globals.ContextPinValue)
|
|
if pinRaw != nil {
|
|
pin = pinRaw.(string)
|
|
}
|
|
|
|
acl := perms.NewACL(parsedGrants...)
|
|
res := perms.Resource{
|
|
// These values were parsed from the request information
|
|
ScopeId: scopeId,
|
|
Type: typ,
|
|
Id: id,
|
|
Pin: pin,
|
|
}
|
|
allowed := acl.Allowed(res, act)
|
|
if allowed.Allowed {
|
|
tMD.authorized = true
|
|
}
|
|
l.Info("grant checking", "user_id", tMD.UserId, "grants", pretty.Sprint(grantPairs), "resource", res, "authorized", allowed)
|
|
|
|
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 Org. 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
|
|
|
|
authorized bool
|
|
|
|
receivedTokenType tokenFormat
|
|
bearerPayload string
|
|
|
|
jsCookiePayload string
|
|
httpCookiePayload string
|
|
}
|
|
|
|
const (
|
|
mdAuthTokenUserKey = "wt-authtoken-user-key"
|
|
mdAuthAuthorizedKey = "wt-authtoken-authorized"
|
|
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 authzd := md.Get(mdAuthAuthorizedKey); len(authzd) > 0 {
|
|
tMD.authorized = authzd[0] == "true"
|
|
}
|
|
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.receivedTokenType = tokenFormat(st)
|
|
}
|
|
}
|
|
|
|
return tMD
|
|
}
|
|
|
|
func (s TokenMetadata) toMetadata() metadata.MD {
|
|
md := metadata.MD{}
|
|
if s.UserId != "" {
|
|
md.Set(mdAuthTokenUserKey, s.UserId)
|
|
}
|
|
if s.authorized {
|
|
md.Set(mdAuthAuthorizedKey, "true")
|
|
} else {
|
|
md.Set(mdAuthAuthorizedKey, "false")
|
|
}
|
|
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.receivedTokenType != authTokenTypeUnknown {
|
|
md.Set(mdAuthTokenTypeKey, fmt.Sprint(s.receivedTokenType))
|
|
}
|
|
|
|
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.receivedTokenType {
|
|
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.receivedTokenType {
|
|
case authTokenTypeBearer:
|
|
tok = s.bearerPayload
|
|
case authTokenTypeSplitCookie:
|
|
tok = s.jsCookiePayload + s.httpCookiePayload
|
|
}
|
|
l := strings.Split(tok, "_")
|
|
if len(l) != 3 {
|
|
return ""
|
|
}
|
|
return l[2]
|
|
}
|