b835de2dc4
* Able to authenticate user against IndieAuth. For #1273 * WIP server indieauth endpoint. For https://github.com/owncast/owncast/issues/1272 * Add migration to remove access tokens from user * Add authenticated bool to user for display purposes * Add indieauth modal and auth flair to display names. For #1273 * Validate URLs and display errors * Renames, cleanups * Handle relative auth endpoint paths. Add error handling for missing redirects. * Disallow using display names in use by registered users. Closes #1810 * Verify code verifier via code challenge on callback * Use relative path to authorization_endpoint * Post-rebase fixes * Use a timestamp instead of a bool for authenticated * Propertly handle and display error in modal * Use auth'ed timestamp to derive authenticated flag to display in chat * don't redirect unless a URL is present avoids redirecting to `undefined` if there was an error * improve error message if owncast server URL isn't set * fix IndieAuth PKCE implementation use SHA256 instead of SHA1, generates a longer code verifier (must be 43-128 chars long), fixes URL-safe SHA256 encoding * return real profile data for IndieAuth response * check the code verifier in the IndieAuth server * Linting * Add new chat settings modal anad split up indieauth ui * Remove logging error * Update the IndieAuth modal UI. For #1273 * Add IndieAuth repsonse error checking * Disable IndieAuth client if server URL is not set. * Add explicit error messages for specific error types * Fix bad logic * Return OAuth-keyed error responses for indieauth server * Display IndieAuth error in plain text with link to return to main page * Remove redundant check * Add additional detail to error * Hide IndieAuth details behind disclosure details * Break out migration into two steps because some people have been runing dev in production * Add auth option to user dropdown Co-authored-by: Aaron Parecki <aaron@parecki.com>
93 lines
2.6 KiB
Go
93 lines
2.6 KiB
Go
package indieauth
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/owncast/owncast/core/data"
|
|
"github.com/pkg/errors"
|
|
"github.com/teris-io/shortid"
|
|
)
|
|
|
|
// ServerAuthRequest is n inbound request to authenticate against
|
|
// this Owncast instance.
|
|
type ServerAuthRequest struct {
|
|
ClientID string
|
|
RedirectURI string
|
|
CodeChallenge string
|
|
State string
|
|
Me string
|
|
Code string
|
|
}
|
|
|
|
// ServerProfile represents basic user-provided data about this Owncast instance.
|
|
type ServerProfile struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"url"`
|
|
Photo string `json:"photo"`
|
|
}
|
|
|
|
// ServerProfileResponse is returned when an auth flow requests the final
|
|
// confirmation of the IndieAuth flow.
|
|
type ServerProfileResponse struct {
|
|
Me string `json:"me,omitempty"`
|
|
Profile ServerProfile `json:"profile,omitempty"`
|
|
// Error keys need to match the OAuth spec.
|
|
Error string `json:"error,omitempty"`
|
|
ErrorDescription string `json:"error_description,omitempty"`
|
|
}
|
|
|
|
var pendingServerAuthRequests = map[string]ServerAuthRequest{}
|
|
|
|
// StartServerAuth will handle the authentication for the admin user of this
|
|
// Owncast server. Initiated via a GET of the auth endpoint.
|
|
// https://indieweb.org/authorization-endpoint
|
|
func StartServerAuth(clientID, redirectURI, codeChallenge, state, me string) (*ServerAuthRequest, error) {
|
|
code := shortid.MustGenerate()
|
|
|
|
r := ServerAuthRequest{
|
|
ClientID: clientID,
|
|
RedirectURI: redirectURI,
|
|
CodeChallenge: codeChallenge,
|
|
State: state,
|
|
Me: me,
|
|
Code: code,
|
|
}
|
|
|
|
pendingServerAuthRequests[code] = r
|
|
|
|
return &r, nil
|
|
}
|
|
|
|
// CompleteServerAuth will verify that the values provided in the final step
|
|
// of the IndieAuth flow are correct, and return some basic profile info.
|
|
func CompleteServerAuth(code, redirectURI, clientID string, codeVerifier string) (*ServerProfileResponse, error) {
|
|
request, pending := pendingServerAuthRequests[code]
|
|
if !pending {
|
|
return nil, errors.New("no pending authentication request")
|
|
}
|
|
|
|
if request.RedirectURI != redirectURI {
|
|
return nil, errors.New("redirect URI does not match")
|
|
}
|
|
|
|
if request.ClientID != clientID {
|
|
return nil, errors.New("client ID does not match")
|
|
}
|
|
|
|
codeChallengeFromRequest := createCodeChallenge(codeVerifier)
|
|
if request.CodeChallenge != codeChallengeFromRequest {
|
|
return nil, errors.New("code verifier is incorrect")
|
|
}
|
|
|
|
response := ServerProfileResponse{
|
|
Me: data.GetServerURL(),
|
|
Profile: ServerProfile{
|
|
Name: data.GetServerName(),
|
|
URL: data.GetServerURL(),
|
|
Photo: fmt.Sprintf("%s/%s", data.GetServerURL(), data.GetLogoPath()),
|
|
},
|
|
}
|
|
|
|
return &response, nil
|
|
}
|