/
authenticator.go
121 lines (113 loc) · 4.62 KB
/
authenticator.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package internal
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"github.com/anderslauri/open-iap/internal/cache"
log "github.com/sirupsen/logrus"
"net/url"
"time"
)
// Authenticator is generic interface for authentication.
type Authenticator interface {
Authenticate(ctx context.Context, credentials string, requestUrl url.URL) error
}
// GoogleCloudTokenAuthenticator is an implementation of Authenticator interface.
type GoogleCloudTokenAuthenticator struct {
token TokenVerifier[*GoogleTokenClaims]
iamClient IdentityAccessManagementReader
gwsClient GoogleWorkspaceClientReader
cache cache.Cache[string, cache.ExpiryCacheValue[GoogleServiceAccount]]
excludedHosts []url.URL
}
// ErrInvalidGoogleCloudAuthentication is given as a general error when Authenticate(...) is not successful.
var ErrInvalidGoogleCloudAuthentication = errors.New("invalid google cloud authentication")
// NewGoogleCloudTokenAuthenticator returns an implementation of interface Authenticator
func NewGoogleCloudTokenAuthenticator(v TokenVerifier[*GoogleTokenClaims], c cache.Cache[string, cache.ExpiryCacheValue[GoogleServiceAccount]], i IdentityAccessManagementReader, g GoogleWorkspaceClientReader, e []url.URL) (*GoogleCloudTokenAuthenticator, error) {
return &GoogleCloudTokenAuthenticator{
token: v,
iamClient: i,
gwsClient: g,
cache: c,
excludedHosts: e,
}, nil
}
// Authenticate verifies if Google credentials are valid.
func (g *GoogleCloudTokenAuthenticator) Authenticate(ctx context.Context, credentials string, requestUrl url.URL) error {
var (
aud = fmt.Sprintf("%s://%s", requestUrl.Scheme, requestUrl.Host)
now = time.Now().Unix()
tokenHash = fmt.Sprintf("%s:%s", credentials, aud)
email GoogleServiceAccount
claims *GoogleTokenClaims
)
for _, host := range g.excludedHosts {
if host.Host == aud {
log.Warningf("Host %s is excluded from authentication.", host.Host)
return nil
}
}
hasher := sha256.New()
// Verify if Google Service Account JWT is present within local cache, if found and exp is valid,
// jump to role binding processing as token requires no re-processing given the fully valid status.
if _, err := hasher.Write([]byte(tokenHash)); err != nil {
log.WithField("error", err).Warning("hasher.Write: returned error. Unexpected.")
} else if entry, ok := g.cache.Get(hex.EncodeToString(hasher.Sum(nil))); ok && entry.Exp < now {
email = entry.Val
goto verifyGoogleCloudPolicyBindings
}
claims = getGoogleTokenClaims()
defer putGoogleTokenClaims(claims)
// Verify token validity, signature and audience.
if err := g.token.Verify(ctx, credentials, aud, claims); err != nil {
log.WithField("error", err).Error("Failed verifying token.")
return err
}
email = GoogleServiceAccount(claims.Email)
// Append to cache.
go g.cache.Set(tokenHash,
cache.ExpiryCacheValue[GoogleServiceAccount]{
Val: email,
Exp: claims.ExpiresAt.Unix(),
})
// Identify if user has role bindings in project.
verifyGoogleCloudPolicyBindings:
bindings, err := g.iamClient.LoadBindingForGoogleServiceAccount(email)
if err != nil {
log.WithField("error", err).Warningf("No policy role binding found for user %s.", email)
return err
} else if len(bindings) == 1 && len(bindings[0].Expression) == 0 {
// We have a single role binding without a conditional expression. User is authenticated.
return nil
}
// Identity Aware Proxy supported parameters for evaluating conditional expression given bindings.
params := map[string]any{
"request.path": requestUrl.Path,
"request.host": requestUrl.Host,
"request.time": now,
}
if len(bindings) == 1 && len(bindings[0].Expression) > 0 {
log.Debugf("User %s has single conditional policy expression. Evaluating.", email)
isAuthorized, err := doesConditionalExpressionEvaluateToTrue(bindings[0].Expression, params)
if !isAuthorized || err != nil {
log.WithField("error", err).Errorf("Conditional expression with title %s is not valid for user %s.",
bindings[0].Title, email)
return ErrInvalidGoogleCloudAuthentication
}
return nil
}
log.Debugf("User %s has multiple conditional policy expressions. Evaluating", email)
for _, binding := range bindings {
if len(binding.Expression) == 0 {
continue
} else if ok, err := doesConditionalExpressionEvaluateToTrue(binding.Expression, params); !ok || err != nil {
log.WithField("error", err).Errorf("Conditional expression %s is not valid for user %s.",
binding.Title, email)
return ErrInvalidGoogleCloudAuthentication
}
}
log.Debugf("Processing successful request with email: %s and audience: %s.", email, requestUrl.String())
return nil
}