mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-11 16:08:09 +00:00
Add passkey enrollment and login wiring, including a Safari-safe explicit login path that prefetches the WebAuthn challenge before the click and calls the credential ceremony immediately from the user gesture.
154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
package config
|
|
|
|
import (
|
|
"context"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
type AuthConfig struct {
|
|
GoogleClientID string
|
|
GoogleRedirectURI string
|
|
GoogleClientSecret string
|
|
|
|
AppleAppID string
|
|
AppleTeamID string
|
|
AppleKeyID string
|
|
AppleKeySecret string
|
|
|
|
AuthSecret string
|
|
TurnstileSecret string
|
|
TurnstileBypass string
|
|
|
|
// WebAuthn / passkey relying-party configuration.
|
|
//
|
|
// WebAuthnRPID is the relying-party ID a passkey is cryptographically
|
|
// bound to — the registrable domain the dashboard is served from
|
|
// (e.g. "app.warmbly.com"), with no scheme or port. WebAuthnRPOrigins
|
|
// are the full scheme-qualified origins allowed to run ceremonies
|
|
// (e.g. "https://app.warmbly.com"). Both are derived from APP_URL /
|
|
// CORS_ALLOW_ORIGINS so self-hosted and local-dev deployments work out
|
|
// of the box, and can be overridden explicitly with WEBAUTHN_RP_ID /
|
|
// WEBAUTHN_RP_ORIGINS. Changing the RP ID invalidates every enrolled
|
|
// passkey, so it must stay stable for a given deployment.
|
|
WebAuthnRPID string
|
|
WebAuthnRPDisplayName string
|
|
WebAuthnRPOrigins []string
|
|
}
|
|
|
|
func (c *Config) LoadAuthConfig(ctx context.Context) (*AuthConfig, error) {
|
|
googleClientID, err := c.GetString(ctx, "GOOGLE_CLIENT_ID", "google-auth/client_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
googleRedirectURI, err := c.GetString(ctx, "GOOGLE_REDIRECT_URI", "google-auth/redirect_uri")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
googleClientSecret, err := c.GetSecret(ctx, "GOOGLE_CLIENT_SECRET", "google-auth/client_secret")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
appleAppID, err := c.GetString(ctx, "APPLE_APP_ID", "apple-auth/app_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
appleTeamID, err := c.GetString(ctx, "APPLE_TEAM_ID", "apple-auth/team_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
appleKeyID, err := c.GetString(ctx, "APPLE_KEY_ID", "apple-auth/key_id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
appleKeySecret, err := c.GetSecret(ctx, "APPLE_KEY_SECRET", "apple-auth/key_secret")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
authSecret, err := c.GetSecret(ctx, "AUTH_SECRET", "auth_secret")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
turnstileSecret, err := c.GetSecret(ctx, "TURNSTILE_SECRET", "turnstile/secret")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
turnstileBypass := c.GetSecretOptional(ctx, "TURNSTILE_BYPASS_TOKEN", "turnstile/bypass_token", "")
|
|
|
|
rpDisplayName := c.GetStringOptional(ctx, "WEBAUTHN_RP_DISPLAY_NAME", "webauthn/rp_display_name", "Warmbly")
|
|
rpIDRaw := c.GetStringOptional(ctx, "WEBAUTHN_RP_ID", "webauthn/rp_id", "")
|
|
rpOriginsRaw := c.GetStringOptional(ctx, "WEBAUTHN_RP_ORIGINS", "webauthn/rp_origins", "")
|
|
rpID, rpOrigins := resolveWebAuthnRP(rpIDRaw, rpOriginsRaw)
|
|
|
|
return &AuthConfig{
|
|
GoogleClientID: googleClientID,
|
|
GoogleClientSecret: googleClientSecret,
|
|
GoogleRedirectURI: googleRedirectURI,
|
|
|
|
AppleAppID: appleAppID,
|
|
AppleTeamID: appleTeamID,
|
|
AppleKeyID: appleKeyID,
|
|
AppleKeySecret: appleKeySecret,
|
|
|
|
AuthSecret: authSecret,
|
|
TurnstileSecret: turnstileSecret,
|
|
TurnstileBypass: turnstileBypass,
|
|
|
|
WebAuthnRPID: rpID,
|
|
WebAuthnRPDisplayName: rpDisplayName,
|
|
WebAuthnRPOrigins: rpOrigins,
|
|
}, nil
|
|
}
|
|
|
|
// resolveWebAuthnRP derives the passkey relying-party ID and allowed origins.
|
|
//
|
|
// Origins come from (in order): an explicit WEBAUTHN_RP_ORIGINS list, then
|
|
// CORS_ALLOW_ORIGINS, then APP_URL, then a localhost dev fallback. The RP ID
|
|
// is an explicit override if given, otherwise the host of the first origin
|
|
// (scheme and port stripped), otherwise "localhost". This keeps prod, self
|
|
// hosted, and local-dev deployments correct without per-environment code.
|
|
func resolveWebAuthnRP(explicitRPID, explicitOrigins string) (string, []string) {
|
|
originsRaw := explicitOrigins
|
|
if originsRaw == "" {
|
|
originsRaw = os.Getenv("CORS_ALLOW_ORIGINS")
|
|
}
|
|
if originsRaw == "" {
|
|
originsRaw = os.Getenv("APP_URL")
|
|
}
|
|
|
|
origins := splitCSV(originsRaw)
|
|
if len(origins) == 0 {
|
|
origins = []string{"http://localhost:5173"}
|
|
}
|
|
|
|
rpID := strings.TrimSpace(explicitRPID)
|
|
if rpID == "" {
|
|
rpID = hostFromOrigin(origins[0])
|
|
}
|
|
if rpID == "" {
|
|
rpID = "localhost"
|
|
}
|
|
|
|
return rpID, origins
|
|
}
|
|
|
|
// hostFromOrigin returns the bare hostname of an origin (no scheme, no port),
|
|
// suitable for use as a WebAuthn RP ID. Returns "" if the origin can't be
|
|
// parsed into a host (e.g. a wildcard "*").
|
|
func hostFromOrigin(origin string) string {
|
|
u, err := url.Parse(strings.TrimSpace(origin))
|
|
if err != nil || u.Hostname() == "" {
|
|
return ""
|
|
}
|
|
return u.Hostname()
|
|
}
|