mirror of
https://github.com/nais/wonderwall.git
synced 2026-05-08 17:37:01 +00:00
203 lines
5.2 KiB
Go
203 lines
5.2 KiB
Go
package session
|
|
|
|
import (
|
|
"encoding"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/nais/wonderwall/pkg/crypto"
|
|
"github.com/nais/wonderwall/pkg/openid"
|
|
)
|
|
|
|
const (
|
|
RefreshMinInterval = 1 * time.Minute
|
|
RefreshLeeway = 5 * time.Minute
|
|
)
|
|
|
|
type EncryptedData struct {
|
|
Data string `json:"data"`
|
|
}
|
|
|
|
var _ encoding.BinaryMarshaler = &EncryptedData{}
|
|
var _ encoding.BinaryUnmarshaler = &EncryptedData{}
|
|
|
|
func (in *EncryptedData) MarshalBinary() ([]byte, error) {
|
|
return json.Marshal(in)
|
|
}
|
|
|
|
func (in *EncryptedData) UnmarshalBinary(bytes []byte) error {
|
|
return json.Unmarshal(bytes, in)
|
|
}
|
|
|
|
func (in *EncryptedData) Decrypt(crypter crypto.Crypter) (*Data, error) {
|
|
ciphertext, err := base64.StdEncoding.DecodeString(in.Data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rawData, err := crypter.Decrypt(ciphertext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var data Data
|
|
err = json.Unmarshal(rawData, &data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &data, nil
|
|
}
|
|
|
|
type Data struct {
|
|
ExternalSessionID string `json:"external_session_id"`
|
|
AccessToken string `json:"access_token"`
|
|
IDToken string `json:"id_token"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
IDTokenJwtID string `json:"id_token_jwt_id"`
|
|
Metadata Metadata `json:"metadata"`
|
|
}
|
|
|
|
func NewData(externalSessionID string, tokens *openid.Tokens, metadata *Metadata) *Data {
|
|
data := &Data{
|
|
ExternalSessionID: externalSessionID,
|
|
AccessToken: tokens.AccessToken,
|
|
IDToken: tokens.IDToken.GetSerialized(),
|
|
IDTokenJwtID: tokens.IDToken.GetJwtID(),
|
|
RefreshToken: tokens.RefreshToken,
|
|
}
|
|
|
|
if metadata != nil {
|
|
data.Metadata = *metadata
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
func (in *Data) Encrypt(crypter crypto.Crypter) (*EncryptedData, error) {
|
|
bytes, err := json.Marshal(in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ciphertext, err := crypter.Encrypt(bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &EncryptedData{
|
|
Data: base64.StdEncoding.EncodeToString(ciphertext),
|
|
}, nil
|
|
}
|
|
|
|
func (in *Data) HasAccessToken() bool {
|
|
return len(in.AccessToken) > 0
|
|
}
|
|
|
|
func (in *Data) HasRefreshToken() bool {
|
|
return len(in.RefreshToken) > 0
|
|
}
|
|
|
|
type Metadata struct {
|
|
// SessionCreatedAt is the time when the session was created.
|
|
SessionCreatedAt time.Time `json:"session_created_at"`
|
|
// SessionEndsAt is the time when the session will end, i.e. the absolute lifetime/time-to-live for the session.
|
|
SessionEndsAt time.Time `json:"session_ends_at"`
|
|
// TokensExpireAt is the time when the tokens within the session expires.
|
|
TokensExpireAt time.Time `json:"tokens_expire_at"`
|
|
// TokensRefreshedAt is the time when the tokens within the session was refreshed.
|
|
TokensRefreshedAt time.Time `json:"tokens_refreshed_at"`
|
|
}
|
|
|
|
func NewMetadata(expiresIn time.Duration, endsIn time.Duration) *Metadata {
|
|
now := time.Now()
|
|
return &Metadata{
|
|
SessionCreatedAt: now,
|
|
SessionEndsAt: now.Add(endsIn),
|
|
TokensRefreshedAt: now,
|
|
TokensExpireAt: now.Add(expiresIn),
|
|
}
|
|
}
|
|
|
|
func (in *Metadata) NextRefresh() time.Time {
|
|
// subtract the leeway to ensure that we refresh before expiry
|
|
next := in.TokensExpireAt.Add(-RefreshLeeway)
|
|
|
|
// try to refresh at the first opportunity if the next refresh is in the past
|
|
if next.Before(time.Now()) {
|
|
return in.RefreshCooldown()
|
|
}
|
|
|
|
return next
|
|
}
|
|
|
|
func (in *Metadata) Refresh(nextExpirySeconds int64) {
|
|
now := time.Now()
|
|
in.TokensRefreshedAt = now
|
|
in.TokensExpireAt = now.Add(time.Duration(nextExpirySeconds) * time.Second)
|
|
}
|
|
|
|
func (in *Metadata) RefreshCooldown() time.Time {
|
|
refreshed := in.TokensRefreshedAt
|
|
tokenLifetime := in.TokenLifetime()
|
|
|
|
// if token lifetime is less than the minimum refresh interval * 2, we'll allow refreshes at the token half-life
|
|
if tokenLifetime <= RefreshMinInterval*2 {
|
|
return refreshed.Add(tokenLifetime / 2)
|
|
}
|
|
|
|
return refreshed.Add(RefreshMinInterval)
|
|
}
|
|
|
|
func (in *Metadata) RefreshOnCooldown() bool {
|
|
return time.Now().Before(in.RefreshCooldown())
|
|
}
|
|
|
|
func (in *Metadata) ShouldRefresh() bool {
|
|
if in.RefreshOnCooldown() {
|
|
return false
|
|
}
|
|
|
|
return time.Now().After(in.NextRefresh())
|
|
}
|
|
|
|
func (in *Metadata) TokenLifetime() time.Duration {
|
|
return in.TokensExpireAt.Sub(in.TokensRefreshedAt)
|
|
}
|
|
|
|
func (in *Metadata) Verbose() MetadataVerbose {
|
|
now := time.Now()
|
|
|
|
expireTime := in.TokensExpireAt
|
|
endTime := in.SessionEndsAt
|
|
nextRefreshTime := in.NextRefresh()
|
|
|
|
return MetadataVerbose{
|
|
Metadata: *in,
|
|
SessionEndsInSeconds: toSeconds(endTime.Sub(now)),
|
|
TokensExpireInSeconds: toSeconds(expireTime.Sub(now)),
|
|
TokensNextRefreshInSeconds: toSeconds(nextRefreshTime.Sub(now)),
|
|
TokensRefreshCooldown: in.RefreshOnCooldown(),
|
|
TokensRefreshCooldownSeconds: toSeconds(in.RefreshCooldown().Sub(now)),
|
|
}
|
|
}
|
|
|
|
type MetadataVerbose struct {
|
|
Metadata
|
|
SessionEndsInSeconds int64 `json:"session_ends_in_seconds"`
|
|
TokensExpireInSeconds int64 `json:"tokens_expire_in_seconds"`
|
|
TokensNextRefreshInSeconds int64 `json:"tokens_next_refresh_in_seconds"`
|
|
TokensRefreshCooldown bool `json:"tokens_refresh_cooldown"`
|
|
TokensRefreshCooldownSeconds int64 `json:"tokens_refresh_cooldown_seconds"`
|
|
}
|
|
|
|
func toSeconds(d time.Duration) int64 {
|
|
i := int64(d.Seconds())
|
|
if i <= 0 {
|
|
return 0
|
|
}
|
|
|
|
return i
|
|
}
|