mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-08-19 11:26:25 +00:00
188 lines
7.3 KiB
Go
188 lines
7.3 KiB
Go
package model
|
|
|
|
import (
|
|
"database/sql/driver"
|
|
"encoding/json"
|
|
"slices"
|
|
"time"
|
|
|
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
|
)
|
|
|
|
type UserAuthorizedOidcClient struct {
|
|
Scope datatype.StringList
|
|
LastUsedAt datatype.DateTime `sortable:"true"`
|
|
|
|
UserID string `gorm:"primary_key;"`
|
|
User User
|
|
|
|
ClientID string `gorm:"primary_key;"`
|
|
Client OidcClient
|
|
}
|
|
|
|
// OidcClientType identifies how an OIDC client was registered.
|
|
type OidcClientType string
|
|
|
|
const (
|
|
OidcClientTypeStandard OidcClientType = "standard"
|
|
OidcClientTypeCIMD OidcClientType = "cimd"
|
|
|
|
// DefaultAccessTokenDurationMinutes is the access-token lifetime used for new clients
|
|
DefaultAccessTokenDurationMinutes int64 = 60
|
|
// DefaultRefreshTokenDurationMinutes is the refresh-token lifetime used for new clients
|
|
DefaultRefreshTokenDurationMinutes int64 = 30 * 24 * 60
|
|
// MinTokenDurationMinutes is the shortest configurable token lifetime
|
|
MinTokenDurationMinutes int64 = 1
|
|
// MaxTokenDurationMinutes is the longest configurable token lifetime
|
|
MaxTokenDurationMinutes int64 = 365 * 24 * 60
|
|
|
|
// MaxOidcClientSecrets is the number of secrets a single client can have at the same time, including expired ones
|
|
MaxOidcClientSecrets = 20
|
|
// OidcClientSecretPrefixLength is how many leading characters of a client secret are kept in clear text so admins can tell secrets apart
|
|
OidcClientSecretPrefixLength = 4
|
|
)
|
|
|
|
type OidcClient struct {
|
|
Base
|
|
|
|
Name string `sortable:"true"`
|
|
Description string
|
|
CallbackURLs datatype.StringList
|
|
LogoutCallbackURLs datatype.StringList
|
|
ImageType *string
|
|
DarkImageType *string
|
|
IsPublic bool
|
|
PkceEnabled bool `sortable:"true" filterable:"true"`
|
|
RequiresReauthentication bool `sortable:"true" filterable:"true"`
|
|
RequiresPushedAuthorizationRequests bool `sortable:"true" filterable:"true"`
|
|
SkipConsent bool `sortable:"true" filterable:"true"`
|
|
Credentials OidcClientCredentials
|
|
LaunchURL *string
|
|
IsGroupRestricted bool `sortable:"true" filterable:"true"`
|
|
PkceSupported bool `sortable:"true" filterable:"true"`
|
|
ClientType OidcClientType `gorm:"default:standard" sortable:"true" filterable:"true"`
|
|
MetadataExpiresAt *datatype.DateTime
|
|
MetadataGrantTypes datatype.StringList
|
|
AccessTokenDurationMinutes int64 `gorm:"default:60"`
|
|
RefreshTokenDurationMinutes int64 `gorm:"default:43200"`
|
|
|
|
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
|
|
CreatedByID *string
|
|
CreatedBy *User
|
|
UserAuthorizedOidcClients []UserAuthorizedOidcClient `gorm:"foreignKey:ClientID;references:ID"`
|
|
}
|
|
|
|
// IsValidTokenDurationMinutes reports whether a duration is within the configurable range
|
|
func IsValidTokenDurationMinutes(minutes int64) bool {
|
|
return minutes >= MinTokenDurationMinutes && minutes <= MaxTokenDurationMinutes
|
|
}
|
|
|
|
func (c OidcClient) HasLogo() bool {
|
|
return c.ImageType != nil && *c.ImageType != ""
|
|
}
|
|
|
|
func (c OidcClient) HasDarkLogo() bool {
|
|
return c.DarkImageType != nil && *c.DarkImageType != ""
|
|
}
|
|
|
|
// IsMetadataDocument reports whether the client was synthesized from an OAuth
|
|
// Client ID Metadata Document. Its ID is then the https URL of the document.
|
|
func (c OidcClient) IsMetadataDocument() bool {
|
|
return c.ClientType == OidcClientTypeCIMD
|
|
}
|
|
|
|
type OidcClientCredentials struct { //nolint:recvcheck
|
|
FederatedIdentities []OidcClientFederatedIdentity `json:"federatedIdentities,omitempty"`
|
|
Secrets []OidcClientSecret `json:"secrets,omitempty"`
|
|
}
|
|
|
|
// OidcClientSecretHashAlgorithm identifies how the hash of a client secret was computed
|
|
type OidcClientSecretHashAlgorithm string
|
|
|
|
const (
|
|
// OidcClientSecretHashSHA256 is used by every client secret generated by Pocket ID
|
|
// A plain hash is enough because client secrets are generated with enough entropy that they cannot be brute-forced
|
|
OidcClientSecretHashSHA256 OidcClientSecretHashAlgorithm = "sha256"
|
|
|
|
// OidcClientSecretHashBcrypt is only found on secrets migrated from the single-secret column that Pocket ID used before it supported multiple secrets
|
|
// Those hashes cannot be converted to SHA-256, so they are kept as-is and verified with bcrypt until the admin rotates the secret
|
|
OidcClientSecretHashBcrypt OidcClientSecretHashAlgorithm = "bcrypt"
|
|
)
|
|
|
|
// OidcClientSecret is a single client secret of an OIDC client, stored hashed in the credentials JSON document
|
|
type OidcClientSecret struct {
|
|
ID string `json:"id"`
|
|
Algorithm OidcClientSecretHashAlgorithm `json:"alg"`
|
|
Hash string `json:"hash"`
|
|
// Prefix is empty for secrets migrated from the single-secret column, whose value was never stored
|
|
Prefix string `json:"prefix,omitempty"`
|
|
CreatedAt datatype.DateTime `json:"createdAt"`
|
|
ExpiresAt *datatype.DateTime `json:"expiresAt,omitempty"`
|
|
}
|
|
|
|
// IsActive reports whether the secret can still be used to authenticate the client
|
|
func (s OidcClientSecret) IsActive() bool {
|
|
return !s.IsExpiredAt(time.Now())
|
|
}
|
|
|
|
// IsExpiredAt reports whether the secret is expired at the given time
|
|
// Secrets without an expiration date are always valid
|
|
func (s OidcClientSecret) IsExpiredAt(now time.Time) bool {
|
|
return s.ExpiresAt != nil && !now.Before(s.ExpiresAt.ToTime())
|
|
}
|
|
|
|
// EncodedHash returns the secret's hash with its algorithm prepended, in the format "<alg>:<hash>"
|
|
// Fosite compares hashes without any other context, so the algorithm has to travel alongside the hash
|
|
func (s OidcClientSecret) EncodedHash() []byte {
|
|
return []byte(string(s.Algorithm) + ":" + s.Hash)
|
|
}
|
|
|
|
// ActiveSecrets returns the secrets that have not expired yet, ordered from the most recently created to the oldest
|
|
func (occ OidcClientCredentials) ActiveSecrets() []OidcClientSecret {
|
|
now := time.Now()
|
|
active := make([]OidcClientSecret, 0, len(occ.Secrets))
|
|
for _, secret := range occ.Secrets {
|
|
if !secret.IsExpiredAt(now) {
|
|
active = append(active, secret)
|
|
}
|
|
}
|
|
|
|
// Sort the copy so the primary hash is always the most recently created active secret
|
|
slices.SortStableFunc(active, func(a, b OidcClientSecret) int {
|
|
return b.CreatedAt.ToTime().Compare(a.CreatedAt.ToTime())
|
|
})
|
|
|
|
return active
|
|
}
|
|
|
|
type OidcClientFederatedIdentity struct {
|
|
Issuer string `json:"issuer"`
|
|
Subject string `json:"subject,omitempty"`
|
|
Audience string `json:"audience,omitempty"`
|
|
JWKS string `json:"jwks,omitempty"` // URL of the JWKS
|
|
ReplayProtection bool `json:"replayProtection,omitempty"`
|
|
}
|
|
|
|
func (occ OidcClientCredentials) FederatedIdentityForIssuer(issuer string) (OidcClientFederatedIdentity, bool) {
|
|
if issuer == "" {
|
|
return OidcClientFederatedIdentity{}, false
|
|
}
|
|
|
|
for _, fi := range occ.FederatedIdentities {
|
|
if fi.Issuer == issuer {
|
|
return fi, true
|
|
}
|
|
}
|
|
|
|
return OidcClientFederatedIdentity{}, false
|
|
}
|
|
|
|
func (occ *OidcClientCredentials) Scan(value any) error {
|
|
return utils.UnmarshalJSONFromDatabase(occ, value)
|
|
}
|
|
|
|
func (occ OidcClientCredentials) Value() (driver.Value, error) {
|
|
return json.Marshal(occ)
|
|
}
|