126 lines
2.8 KiB
Go
126 lines
2.8 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"net/http"
|
|
"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 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)
|
|
}
|