Documentation
¶
Overview ¶
Package oidc implements login with external identity providers (GitHub, Google, generic OpenID Connect with discovery) for both the RustDesk client (/api/oidc/*, polling) and the panel (cookie session), plus linking identities to existing accounts.
Security properties:
- state is a 256-bit random value stored only as sha256 in oauth_states, 5 minute TTL, single use (atomic claim/consume);
- PKCE (S256 by default) with the verifier AEAD-encrypted at rest;
- a nonce bound into the ID token; ID tokens are verified (asymmetric algorithms only, iss/aud/azp/exp/iat/nonce);
- the redirect URI is always <public_url>/api/oidc/callback, never derived from request headers;
- client polls are bound to the requesting device id+uuid;
- panel flows are bound to the browser by an HttpOnly cookie (only its sha256 is stored on the state row). The IdP returns to the public host; the callback stores the result and hands off to the admin host with a single-use ticket (HandoffTTL), where the cookie is verified before any session is created or identity linked (login CSRF protection, works when the panel and the client API are served from different hosts);
- an identity is linked to an existing user by email only when the IdP asserts email_verified=true and the provider enables link_by_verified_email.
Index ¶
- Constants
- Variables
- func ClientUserStatus(u *store.User) int
- func Register(r chi.Router, d RouteDeps)
- func SafeRedirectPath(p, def string) string
- func SupportedKind(k store.ProviderKind) bool
- type AuthBody
- type ClientDevice
- type ClientLoginFunc
- type GitHubEndpoints
- type Metadata
- type Options
- type Profile
- type RouteDeps
- type Service
- func (s *Service) BeginBind(ctx context.Context, w http.ResponseWriter, slug string, userID int64, ...) (string, error)
- func (s *Service) BeginClient(ctx context.Context, slug string, dev ClientDevice) (code, authURL string, err error)
- func (s *Service) BeginWebLogin(ctx context.Context, w http.ResponseWriter, slug, next string) (string, error)
- func (s *Service) CallbackURL() string
- func (s *Service) CompleteWeb(w http.ResponseWriter, r *http.Request)
- func (s *Service) Discover(ctx context.Context, p *store.IdentityProvider) (*Metadata, error)
- func (s *Service) HandleCallback(w http.ResponseWriter, r *http.Request)
- func (s *Service) LoginOptions(ctx context.Context) ([]string, error)
- func (s *Service) Poll(ctx context.Context, code, id, uuid string, r *http.Request) any
- type UserInfo
- type UserPayload
- type WebLoginFunc
Constants ¶
const ( // PendingError keeps the client polling (substring match on the client side). PendingError = "No authed oidc is found" // InvalidCodeError ends the attempt (unknown, expired, used or foreign code). InvalidCodeError = "Invalid or expired code" )
Exact wire strings of the RustDesk client contract (docs/research/client-api.md §9).
const BindingCookie = "rdpanel_oauth"
BindingCookie binds a panel flow to the browser that started it.
const CallbackPath = "/api/oidc/callback"
CallbackPath is the redirect URI path registered at every IdP (legacy compatible: production IdP registrations contain exactly this path).
const DefaultBindReturn = "/my/profile"
DefaultBindReturn is where the browser returns after linking an identity.
const DefaultStateTTL = 5 * time.Minute
DefaultStateTTL is the lifetime of a pending OAuth flow.
const GoogleIssuer = "https://accounts.google.com"
GoogleIssuer is the fixed issuer of kind "google" providers.
const HandoffPath = "/api/v1/admin/auth/oidc/complete"
HandoffPath is the admin-host endpoint completing panel flows.
const HandoffTTL = 2 * time.Minute
HandoffTTL bounds the hand-off ticket of a panel flow (callback on the public host → GET /api/v1/admin/auth/oidc/complete on the admin host).
const LoginErrorPath = "/login"
LoginErrorPath is the SPA route panel OIDC failures redirect to.
Variables ¶
var ( ErrUnknownProvider = errors.New("unknown or disabled identity provider") ErrNotConfigured = errors.New("OIDC is not available: public URL is not configured") ErrProviderUnavailable = errors.New("identity provider is unavailable") )
Errors returned to callers (messages are safe to show).
var DefaultGitHub = GitHubEndpoints{
AuthURL: "https://github.com/login/oauth/authorize",
TokenURL: "https://github.com/login/oauth/access_token",
APIURL: "https://api.github.com",
}
DefaultGitHub are the public github.com endpoints.
Functions ¶
func ClientUserStatus ¶
ClientUserStatus maps a user status to the client enum (1 normal, 0 disabled).
func Register ¶
Register mounts the RustDesk client OIDC endpoints on r (already scoped to /api). Stream A's clientapi must not register these paths:
GET /login-options POST /oidc/auth GET /oidc/auth-query GET /oidc/callback (+ legacy alias /oauth/callback)
func SafeRedirectPath ¶
SafeRedirectPath returns p when it is a same-origin relative path ("/x?y#z", not "//host", "/\host" or anything with a scheme), else def.
func SupportedKind ¶
func SupportedKind(k store.ProviderKind) bool
SupportedKind reports whether kind can be used for logins.
Types ¶
type AuthBody ¶
type AuthBody struct {
AccessToken string `json:"access_token"`
Type string `json:"type"`
User UserPayload `json:"user"`
}
AuthBody is the auth-query success body.
type ClientDevice ¶
type ClientDevice struct {
ID, UUID, Name, OS, Type string
}
ClientDevice identifies the RustDesk device starting a client flow.
type ClientLoginFunc ¶
type ClientLoginFunc func(ctx context.Context, user *store.User, tokenID int64, st *store.OAuthState)
ClientLoginFunc is called after a client token was issued via OIDC (e.g. to bind the device to the user). Optional.
type GitHubEndpoints ¶
type GitHubEndpoints struct {
AuthURL string
TokenURL string
APIURL string // base for /user and /user/emails
}
GitHubEndpoints are the OAuth endpoints of GitHub (overridable in tests).
type Metadata ¶
type Metadata struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"`
JWKSURI string `json:"jwks_uri"`
TokenEndpointAuthMethods []string `json:"token_endpoint_auth_methods_supported,omitempty"`
CodeChallengeMethods []string `json:"code_challenge_methods_supported,omitempty"`
}
Metadata is the subset of an OpenID Provider configuration we use.
type Options ¶
type Options struct {
Store *store.Store
AEAD *crypto.AEAD
JWT *auth.ClientJWT
Log *slog.Logger
// PublicURL is the configured external base URL (no trailing slash).
PublicURL string
// AdminURL is the panel base URL (default PublicURL); panel flows are
// handed off there after the IdP callback.
AdminURL string
// TokenLifetime is the sliding lifetime of client tokens; JWTLifetime the
// fixed JWT exp lifetime (0 = TokenLifetime).
TokenLifetime time.Duration
JWTLifetime time.Duration
// StateTTL defaults to DefaultStateTTL.
StateTTL time.Duration
// HTTPClient talks to identity providers (default: 10 s timeout).
HTTPClient *http.Client
// SecureCookies sets the Secure attribute on the browser-binding cookie.
SecureCookies bool
WebLogin WebLoginFunc
OnClientLogin ClientLoginFunc
// LoginEvents records login attempts (default: loginlog.DB on the store).
LoginEvents loginlog.Recorder
// Telemetry counts OIDC logins and issued client tokens (nil: none).
Telemetry *metrics.Panel
// GitHub overrides the GitHub endpoints (tests); GoogleIssuer the Google issuer.
GitHub *GitHubEndpoints
GoogleIssuer string
}
Options configure a Service.
type Profile ¶
type Profile struct {
Subject string `json:"subject"`
Email string `json:"email,omitempty"`
EmailVerified bool `json:"email_verified"`
Username string `json:"username,omitempty"`
DisplayName string `json:"display_name,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
}
Profile is the identity asserted by a provider after a successful login.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service runs OAuth/OIDC flows. Safe for concurrent use.
func (*Service) BeginBind ¶
func (s *Service) BeginBind(ctx context.Context, w http.ResponseWriter, slug string, userID int64, next string) (string, error)
BeginBind starts linking an identity of provider slug to userID.
func (*Service) BeginClient ¶
func (s *Service) BeginClient(ctx context.Context, slug string, dev ClientDevice) (code, authURL string, err error)
BeginClient starts a RustDesk client login (POST /api/oidc/auth). The returned code is the poll code (= the OAuth state).
func (*Service) BeginWebLogin ¶
func (s *Service) BeginWebLogin(ctx context.Context, w http.ResponseWriter, slug, next string) (string, error)
BeginWebLogin starts a panel login and sets the browser-binding cookie. It returns the IdP URL the browser must navigate to.
func (*Service) CallbackURL ¶
CallbackURL returns the redirect URI admins must register at the IdP ("" when the public URL is not configured).
func (*Service) CompleteWeb ¶
func (s *Service) CompleteWeb(w http.ResponseWriter, r *http.Request)
CompleteWeb serves GET /api/v1/admin/auth/oidc/complete?ticket=… on the admin host: it consumes the single-use ticket, verifies the browser-binding cookie against the flow (constant time) and then signs the user in or links the identity, redirecting into the SPA.
func (*Service) Discover ¶
Discover runs (uncached) discovery for a provider row: the admin "test" action.
func (*Service) HandleCallback ¶
func (s *Service) HandleCallback(w http.ResponseWriter, r *http.Request)
HandleCallback serves GET /api/oidc/callback (the IdP redirect target).
func (*Service) LoginOptions ¶
LoginOptions returns the login-options array: one "common-oidc/<json>" item (wins in the client) plus one "oidc/<slug>" per enabled provider.
func (*Service) Poll ¶
Poll implements GET /api/oidc/auth-query. It always returns a JSON body: {"error":"No authed oidc is found"} while pending, {"error": msg} on failure, or an AuthBody once (the code is consumed on the first answer that is not "pending"). The poll must present the same device id and uuid that started the flow; otherwise the code is not touched.
type UserInfo ¶
type UserInfo struct {
EmailVerification bool `json:"email_verification"`
EmailAlarmNotification bool `json:"email_alarm_notification"`
}
UserInfo is the required "info" object of the client UserPayload.
type UserPayload ¶
type UserPayload struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Avatar string `json:"avatar"`
Email string `json:"email"`
Note string `json:"note"`
IsAdmin bool `json:"is_admin"`
Status int `json:"status"`
Info UserInfo `json:"info"`
}
UserPayload is the user object of the client auth body. The Rust client requires "name" and "info" and accepts status only in {0, 1, -1}.
func NewUserPayload ¶
func NewUserPayload(u *store.User) UserPayload
NewUserPayload builds the client user object.
type WebLoginFunc ¶
type WebLoginFunc func(w http.ResponseWriter, r *http.Request, user *store.User, redirectPath string)
WebLoginFunc completes a panel OIDC login: it must create the session (rotating any existing one), set cookies and write the redirect response. redirectPath is already validated as a safe relative path.