package clientapi import ( "crypto/rand" "encoding/base64" "errors" "fmt" "net/http" "time" "github.com/dariuszjaniak/warp/server/internal/httputil" "github.com/dariuszjaniak/warp/server/internal/storage" "github.com/dariuszjaniak/warp/server/internal/userapi" ) // openIDTokenLifetime is how long an OpenID token is good for. The LiveKit // JWT service checks it once, when a call is joined, so an hour is plenty. const openIDTokenLifetime = time.Hour // openIDTokenPrefix marks OpenID tokens the way the userapi marks access // tokens, so one handed to the wrong endpoint is recognisable. const openIDTokenPrefix = "warp_oi_" // openIDUserInfoPath is where a third party exchanges an OpenID token for // the user's ID. It is a federation endpoint by name, but needs neither // federation nor a signing key: Synapse serves it as a resource of its own // for the same reason. It is registered here so that a server with // federation off can still let its users into a group call. const openIDUserInfoPath = "/_matrix/federation/v1/openid/userinfo" // registerOpenIDRoutes adds the OpenID endpoints, if there is a store for // the tokens. func (c *ClientAPI) registerOpenIDRoutes(r *httputil.Router) { if c.openID == nil { return } c.authed(r, "POST "+v3+"/user/{userID}/openid/request_token", c.limitDefault, c.handleRequestOpenIDToken) c.public(r, "GET "+openIDUserInfoPath, c.limitDefault, c.handleOpenIDUserInfo) } type openIDTokenResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` MatrixServerName string `json:"matrix_server_name"` ExpiresIn int64 `json:"expires_in"` } // handleRequestOpenIDToken implements // POST /_matrix/client/v3/user/{userID}/openid/request_token. // // The token proves who the user is to whoever they hand it to, and nothing // else: it is not an access token and opens no other endpoint. func (c *ClientAPI) handleRequestOpenIDToken(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { if r.PathValue("userID") != auth.UserID { return httputil.Forbidden("You cannot request an OpenID token for another user") } // The body is an empty object; anything in it is ignored. var ignored map[string]any if _, err := httputil.DecodeOptionalJSON(w, r, &ignored); err != nil { return err } raw := make([]byte, 32) if _, err := rand.Read(raw); err != nil { return fmt.Errorf("generate OpenID token: %w", err) } token := openIDTokenPrefix + base64.RawURLEncoding.EncodeToString(raw) now := c.now() // Expired tokens go as new ones are made; nothing else reads them. if err := c.openID.PurgeOpenIDTokens(r.Context(), now.UnixMilli()); err != nil { c.log.Warn("could not purge expired OpenID tokens", "error", err) } if err := c.openID.InsertOpenIDToken(r.Context(), userapi.HashToken(token), auth.UserID, now.Add(openIDTokenLifetime).UnixMilli()); err != nil { return err } httputil.WriteJSON(w, http.StatusOK, openIDTokenResponse{ AccessToken: token, TokenType: "Bearer", MatrixServerName: c.serverName, ExpiresIn: int64(openIDTokenLifetime.Seconds()), }) return nil } type openIDUserInfoResponse struct { Sub string `json:"sub"` } // handleOpenIDUserInfo implements // GET /_matrix/federation/v1/openid/userinfo. func (c *ClientAPI) handleOpenIDUserInfo(w http.ResponseWriter, r *http.Request) error { token := r.URL.Query().Get("access_token") if token == "" { return httputil.MissingToken() } userID, err := c.openID.OpenIDTokenUser(r.Context(), userapi.HashToken(token), c.now().UnixMilli()) if errors.Is(err, storage.ErrNoSuchOpenIDToken) { return httputil.UnknownToken(false) } if err != nil { return err } httputil.WriteJSON(w, http.StatusOK, openIDUserInfoResponse{Sub: userID}) return nil }