mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-29 10:26:29 +02:00
- import path comments are obsoleted by the go.mod file's module statement Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
184 lines
5.5 KiB
Go
184 lines
5.5 KiB
Go
package sessions
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cespare/xxhash/v2"
|
|
oidc "github.com/coreos/go-oidc"
|
|
"github.com/mitchellh/hashstructure"
|
|
"golang.org/x/oauth2"
|
|
"gopkg.in/square/go-jose.v2/jwt"
|
|
)
|
|
|
|
// timeNow is time.Now but pulled out as a variable for tests.
|
|
var timeNow = time.Now
|
|
|
|
// State is our object that keeps track of a user's session state
|
|
type State struct {
|
|
// Public claim values (as specified in RFC 7519).
|
|
Issuer string `json:"iss,omitempty"`
|
|
Subject string `json:"sub,omitempty"`
|
|
Audience jwt.Audience `json:"aud,omitempty"`
|
|
Expiry *jwt.NumericDate `json:"exp,omitempty"`
|
|
NotBefore *jwt.NumericDate `json:"nbf,omitempty"`
|
|
IssuedAt *jwt.NumericDate `json:"iat,omitempty"`
|
|
ID string `json:"jti,omitempty"`
|
|
|
|
// core pomerium identity claims ; not standard to RFC 7519
|
|
Email string `json:"email"`
|
|
Groups []string `json:"groups,omitempty"`
|
|
User string `json:"user,omitempty"` // google
|
|
|
|
// commonly supported IdP information
|
|
// https://www.iana.org/assignments/jwt/jwt.xhtml#claims
|
|
Name string `json:"name,omitempty"` // google
|
|
GivenName string `json:"given_name,omitempty"` // google
|
|
FamilyName string `json:"family_name,omitempty"` // google
|
|
Picture string `json:"picture,omitempty"` // google
|
|
EmailVerified bool `json:"email_verified,omitempty"` // google
|
|
|
|
// Impersonate-able fields
|
|
ImpersonateEmail string `json:"impersonate_email,omitempty"`
|
|
ImpersonateGroups []string `json:"impersonate_groups,omitempty"`
|
|
|
|
// Programmatic whether this state is used for machine-to-machine
|
|
// programatic access.
|
|
Programmatic bool `json:"programatic"`
|
|
|
|
AccessToken *oauth2.Token `json:"act,omitempty"`
|
|
AccessTokenID string `json:"ati,omitempty"`
|
|
|
|
idToken *oidc.IDToken
|
|
}
|
|
|
|
// NewStateFromTokens returns a session state built from oidc and oauth2
|
|
// tokens as part of OpenID Connect flow with a new audience appended to the
|
|
// audience claim.
|
|
func NewStateFromTokens(idToken *oidc.IDToken, accessToken *oauth2.Token, audience string) (*State, error) {
|
|
if idToken == nil {
|
|
return nil, errors.New("sessions: oidc id token missing")
|
|
}
|
|
if accessToken == nil {
|
|
return nil, errors.New("sessions: oauth2 token missing")
|
|
}
|
|
s := &State{}
|
|
if err := idToken.Claims(s); err != nil {
|
|
return nil, fmt.Errorf("sessions: couldn't unmarshal extra claims %w", err)
|
|
}
|
|
s.Audience = []string{audience}
|
|
s.idToken = idToken
|
|
s.AccessToken = accessToken
|
|
s.AccessTokenID = s.accessTokenHash()
|
|
return s, nil
|
|
}
|
|
|
|
// UpdateState updates the current state given a new identity (oidc) and authorization
|
|
// (oauth2) tokens following a oidc refresh. NB, unlike during authentication,
|
|
// refresh typically provides fewer claims in the token so we want to build from
|
|
// our previous state.
|
|
func (s *State) UpdateState(idToken *oidc.IDToken, accessToken *oauth2.Token) error {
|
|
if idToken == nil {
|
|
return errors.New("sessions: oidc id token missing")
|
|
}
|
|
if accessToken == nil {
|
|
return errors.New("sessions: oauth2 token missing")
|
|
}
|
|
audience := append(s.Audience[:0:0], s.Audience...)
|
|
s.AccessToken = accessToken
|
|
if err := idToken.Claims(s); err != nil {
|
|
return fmt.Errorf("sessions: update state failed %w", err)
|
|
}
|
|
s.Audience = audience
|
|
s.Expiry = jwt.NewNumericDate(accessToken.Expiry)
|
|
s.AccessTokenID = s.accessTokenHash()
|
|
return nil
|
|
}
|
|
|
|
// NewSession updates issuer, audience, and issuance timestamps but keeps
|
|
// parent expiry.
|
|
func (s State) NewSession(issuer string, audience []string) *State {
|
|
s.IssuedAt = jwt.NewNumericDate(timeNow())
|
|
s.NotBefore = s.IssuedAt
|
|
s.Audience = audience
|
|
s.Issuer = issuer
|
|
return &s
|
|
}
|
|
|
|
// RouteSession creates a route session with access tokens stripped.
|
|
func (s State) RouteSession() *State {
|
|
s.AccessToken = nil
|
|
return &s
|
|
}
|
|
|
|
// Verify returns an error if the users's session state is not valid.
|
|
func (s *State) Verify(audience string) error {
|
|
if s.NotBefore != nil && timeNow().Before(s.NotBefore.Time()) {
|
|
return ErrNotValidYet
|
|
}
|
|
|
|
if s.Expiry != nil && timeNow().After(s.Expiry.Time()) {
|
|
return ErrExpired
|
|
}
|
|
|
|
if s.IssuedAt != nil && !timeNow().Equal(s.IssuedAt.Time()) && timeNow().Before(s.IssuedAt.Time()) {
|
|
return ErrIssuedInTheFuture
|
|
}
|
|
|
|
// if we have an associated access token, check if that token has expired as well
|
|
if s.AccessToken != nil && timeNow().After(s.AccessToken.Expiry) {
|
|
return ErrExpired
|
|
}
|
|
|
|
if len(s.Audience) != 0 {
|
|
if !s.Audience.Contains(audience) {
|
|
return ErrInvalidAudience
|
|
}
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Impersonating returns if the request is impersonating.
|
|
func (s *State) Impersonating() bool {
|
|
return s.ImpersonateEmail != "" || len(s.ImpersonateGroups) != 0
|
|
}
|
|
|
|
// RequestEmail is the email to make the request as.
|
|
func (s *State) RequestEmail() string {
|
|
if s.ImpersonateEmail != "" {
|
|
return s.ImpersonateEmail
|
|
}
|
|
return s.Email
|
|
}
|
|
|
|
// RequestGroups returns the groups of the Groups making the request; uses
|
|
// impersonating user if set.
|
|
func (s *State) RequestGroups() string {
|
|
if len(s.ImpersonateGroups) != 0 {
|
|
return strings.Join(s.ImpersonateGroups, ",")
|
|
}
|
|
return strings.Join(s.Groups, ",")
|
|
}
|
|
|
|
// SetImpersonation sets impersonation user and groups.
|
|
func (s *State) SetImpersonation(email, groups string) {
|
|
s.ImpersonateEmail = email
|
|
if groups == "" {
|
|
s.ImpersonateGroups = nil
|
|
} else {
|
|
s.ImpersonateGroups = strings.Split(groups, ",")
|
|
}
|
|
}
|
|
|
|
func (s *State) accessTokenHash() string {
|
|
hash, err := hashstructure.Hash(
|
|
s.AccessToken,
|
|
&hashstructure.HashOptions{Hasher: xxhash.New()})
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%x", hash)
|
|
}
|