feat: add GitHub OAuth2 provider branch

This commit is contained in:
2026-08-03 10:48:19 +01:00
parent f1c3f67864
commit f3b9f6f286
+83 -5
View File
@@ -2,19 +2,97 @@ package auth
import (
"context"
"encoding/json"
"errors"
"net/http"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
"golang.org/x/oauth2"
)
// githubOAuthConfig and githubIdentity are temporary stubs. Task 7 replaces
// this file with the real GitHub OAuth2 provider implementation.
// githubAPIBase is a variable rather than a constant so tests can point it at
// an httptest server. Nothing in production reassigns it.
var githubAPIBase = "https://api.github.com"
const (
githubAuthURL = "https://github.com/login/oauth/authorize"
githubTokenURL = "https://github.com/login/oauth/access_token"
)
type githubEmail struct {
Email string `json:"email"`
Primary bool `json:"primary"`
Verified bool `json:"verified"`
}
// githubOAuthConfig builds the OAuth2 config for a GitHub provider. GitHub has
// no discovery document, so the endpoints are constants rather than fetched.
func githubOAuthConfig(p *models.AuthProvider, secret, redirectURL string) *oauth2.Config {
return nil
return &oauth2.Config{
ClientID: p.ClientID,
ClientSecret: secret,
RedirectURL: redirectURL,
Scopes: p.Scopes,
Endpoint: oauth2.Endpoint{
AuthURL: githubAuthURL,
TokenURL: githubTokenURL,
},
}
}
func githubIdentity(ctx context.Context, cfg *oauth2.Config, token *oauth2.Token) (string, string, error) {
return "", "", errors.New("not implemented")
// selectGitHubEmail requires an address that is both primary and verified.
//
// Verified alone is not enough: a non-primary address is one the person happens
// to have proved, not the one they present as themselves. Primary alone is far
// worse — an unverified address is not proof of control at all, and accepting
// one would let anyone with a GitHub account claim any address in the instance.
func selectGitHubEmail(emails []githubEmail) (string, error) {
for _, e := range emails {
if e.Primary && e.Verified && e.Email != "" {
return e.Email, nil
}
}
return "", errors.New("no primary verified email address on the GitHub account")
}
func githubGetJSON(ctx context.Context, client *http.Client, url string, out any) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
req.Header.Set("Accept", "application/vnd.github+json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New("github api returned " + resp.Status)
}
return json.NewDecoder(resp.Body).Decode(out)
}
// githubIdentity resolves the signed-in GitHub account to an email and a name.
//
// The name is best-effort: it is cosmetic, and a failing /user must not fail a
// sign-in whose identity is already established.
func githubIdentity(ctx context.Context, cfg *oauth2.Config, token *oauth2.Token) (string, string, error) {
client := cfg.Client(ctx, token)
var emails []githubEmail
if err := githubGetJSON(ctx, client, githubAPIBase+"/user/emails", &emails); err != nil {
return "", "", err
}
email, err := selectGitHubEmail(emails)
if err != nil {
return "", "", err
}
var user struct {
Name string `json:"name"`
}
if err := githubGetJSON(ctx, client, githubAPIBase+"/user", &user); err != nil {
return email, "", nil
}
return email, user.Name, nil
}