You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
126 lines
3.4 KiB
126 lines
3.4 KiB
package indieauth |
|
|
|
import ( |
|
"crypto/sha256" |
|
"encoding/base64" |
|
"fmt" |
|
"net/http" |
|
"net/url" |
|
"strings" |
|
"time" |
|
|
|
"github.com/andybalholm/cascadia" |
|
"github.com/pkg/errors" |
|
"golang.org/x/net/html" |
|
) |
|
|
|
func createAuthRequest(authDestination, userID, displayName, accessToken, baseServer string) (*Request, error) { |
|
authURL, err := url.Parse(authDestination) |
|
if err != nil { |
|
return nil, errors.Wrap(err, "unable to parse IndieAuth destination") |
|
} |
|
|
|
authEndpointURL, err := getAuthEndpointFromURL(authURL.String()) |
|
if err != nil { |
|
return nil, errors.Wrap(err, "unable to get IndieAuth endpoint from destination URL") |
|
} |
|
|
|
baseServerURL, err := url.Parse(baseServer) |
|
if err != nil { |
|
return nil, errors.Wrap(err, "unable to parse local owncast base server URL") |
|
} |
|
|
|
callbackURL := *baseServerURL |
|
callbackURL.Path = "/api/auth/indieauth/callback" |
|
|
|
codeVerifier := randString(50) |
|
codeChallenge := createCodeChallenge(codeVerifier) |
|
state := randString(20) |
|
responseType := "code" |
|
clientID := baseServerURL.String() // Our local URL |
|
codeChallengeMethod := "S256" |
|
|
|
redirect := *authEndpointURL |
|
|
|
q := authURL.Query() |
|
q.Add("response_type", responseType) |
|
q.Add("client_id", clientID) |
|
q.Add("state", state) |
|
q.Add("code_challenge_method", codeChallengeMethod) |
|
q.Add("code_challenge", codeChallenge) |
|
q.Add("me", authURL.String()) |
|
q.Add("redirect_uri", callbackURL.String()) |
|
redirect.RawQuery = q.Encode() |
|
|
|
return &Request{ |
|
Me: authURL, |
|
UserID: userID, |
|
DisplayName: displayName, |
|
CurrentAccessToken: accessToken, |
|
Endpoint: authEndpointURL, |
|
ClientID: baseServer, |
|
CodeVerifier: codeVerifier, |
|
CodeChallenge: codeChallenge, |
|
State: state, |
|
Redirect: &redirect, |
|
Callback: &callbackURL, |
|
Timestamp: time.Now(), |
|
}, nil |
|
} |
|
|
|
func getAuthEndpointFromURL(urlstring string) (*url.URL, error) { |
|
htmlDocScrapeURL, err := url.Parse(urlstring) |
|
if err != nil { |
|
return nil, errors.Wrap(err, "unable to parse URL") |
|
} |
|
|
|
if htmlDocScrapeURL.Scheme != "https" { |
|
return nil, fmt.Errorf("url must be https") |
|
} |
|
|
|
r, err := http.Get(htmlDocScrapeURL.String()) // nolint:gosec |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer r.Body.Close() |
|
|
|
scrapedHTMLDocument, err := html.Parse(r.Body) |
|
if err != nil { |
|
return nil, errors.Wrap(err, "unable to parse html at remote auth host") |
|
} |
|
authorizationEndpointTag := cascadia.MustCompile("link[rel=authorization_endpoint]").MatchAll(scrapedHTMLDocument) |
|
if len(authorizationEndpointTag) == 0 { |
|
return nil, fmt.Errorf("url does not support indieauth") |
|
} |
|
|
|
for _, attr := range authorizationEndpointTag[len(authorizationEndpointTag)-1].Attr { |
|
if attr.Key == "href" { |
|
u, err := url.Parse(attr.Val) |
|
if err != nil { |
|
return nil, errors.Wrap(err, "unable to parse authorization endpoint") |
|
} |
|
|
|
// If it is a relative URL we an fill in the missing components |
|
// by using the original URL we scraped, since it is the same host. |
|
if u.Scheme == "" { |
|
u.Scheme = htmlDocScrapeURL.Scheme |
|
} |
|
|
|
if u.Host == "" { |
|
u.Host = htmlDocScrapeURL.Host |
|
} |
|
|
|
return u, nil |
|
} |
|
} |
|
|
|
return nil, fmt.Errorf("unable to find href value for authorization_endpoint") |
|
} |
|
|
|
func createCodeChallenge(codeVerifier string) string { |
|
sha256hash := sha256.Sum256([]byte(codeVerifier)) |
|
|
|
encodedHashedCode := strings.TrimRight(base64.URLEncoding.EncodeToString(sha256hash[:]), "=") |
|
|
|
return encodedHashedCode |
|
}
|
|
|