package auth import ( "context" "errors" "log" "net/http" "net/url" "slices" "time" "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" "tomatentum.net/outfit-voting-abi26/internal/util" ) var ( provider *oidc.Provider verifier *oidc.IDTokenVerifier config oauth2.Config ctx context.Context authCookie string = "outfit-voting-abi26-auth" ) func IssuerBaseURL() string { urlstring := provider.Endpoint().AuthURL url, err := url.Parse(urlstring) if err != nil { return "" } url.Path = "/" return url.String() } func InitProvider(issuer, id, secret, redirect string) error { ctx = context.Background() var err error provider, err = oidc.NewProvider(ctx, issuer) if err != nil { return err } verifier = provider.Verifier(&oidc.Config{ ClientID: id, }) config = oauth2.Config{ ClientID: id, ClientSecret: secret, Endpoint: provider.Endpoint(), RedirectURL: redirect, Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } return nil } func GetToken(code string) (*oidc.IDToken, error) { token, err := config.Exchange(ctx, code) if err != nil { return &oidc.IDToken{}, err } rawToken, ok := token.Extra("id_token").(string) if !ok { return &oidc.IDToken{}, errors.New("id_token missing") } return verifier.Verify(ctx, rawToken) } func Redirect(w http.ResponseWriter, r *http.Request) { state, nonce := setCookies(w, r) AddRedirect(state, r.RequestURI) url := config.AuthCodeURL(state, oidc.Nonce(nonce)) http.Redirect(w, r, url, http.StatusFound) } func CheckState(r *http.Request) error { cookie, err := r.Cookie("state") if err != nil { return errors.Join(err, errors.New("State not found")) } log.Printf("DEBUG: statecookie: %s, statequery: %s\n", cookie.Value, r.URL.Query().Get("state")) if r.URL.Query().Get("state") != cookie.Value { return errors.New("State did not match") } return nil } func CheckNonce(r *http.Request, idtoken *oidc.IDToken) error { cookie, err := r.Cookie("nonce") if err != nil { return errors.Join(err, errors.New("Nonce not found")) } if idtoken.Nonce != cookie.Value { return errors.New("Nonce did not match") } return nil } func CheckAudience(idtoken *oidc.IDToken) error { if !slices.Contains(idtoken.Audience, config.ClientID) { return errors.New("Audience does not match") } return nil } func setCookies(w http.ResponseWriter, r *http.Request) (state, nonce string) { state, err := util.RandString(16) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } nonce, err = util.RandString(16) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } setCallbackCookie(w, r, "state", state) setCallbackCookie(w, r, "nonce", nonce) return state, nonce } func setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) { c := &http.Cookie{ Name: name, Value: value, MaxAge: int(time.Hour.Seconds()), Secure: r.TLS != nil, HttpOnly: true, } http.SetCookie(w, c) }