feat(*): initial commit
build / Go-Build (push) Successful in 1m1s

This commit is contained in:
2026-06-24 00:16:21 +02:00
parent 3cf9e6f266
commit 85e4df52a5
20 changed files with 1125 additions and 3 deletions
+54
View File
@@ -0,0 +1,54 @@
package auth
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
var signingkey []byte
type JWTClaims struct {
User string `json:"user"`
jwt.RegisteredClaims
}
func SetSigningKey(key string) {
signingkey = []byte(key)
}
func NewJWT(user string) (string, error) {
claims := JWTClaims{
user,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)), // Expires in 24 hours
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "outfit-voting-abi26",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(signingkey)
}
func ValidateJWT(token string) (string, error) {
claims := &JWTClaims{}
parsed, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return signingkey, nil
})
if err != nil {
return "", err
}
claims, ok := parsed.Claims.(*JWTClaims)
if !ok {
return "", jwt.ErrTokenInvalidClaims
}
return claims.User, nil
}
+125
View File
@@ -0,0 +1,125 @@
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)
}
+22
View File
@@ -0,0 +1,22 @@
package auth
import (
"sync"
)
var callbackRedirectMap map[string]string = make(map[string]string, 5)
var callbackRedirectMutex sync.Mutex
func AddRedirect(state, url string) {
callbackRedirectMutex.Lock()
callbackRedirectMap[state] = url
callbackRedirectMutex.Unlock()
}
func GetRedirect(state string) string {
callbackRedirectMutex.Lock()
redirect := callbackRedirectMap[state]
delete(callbackRedirectMap, state)
callbackRedirectMutex.Unlock()
return redirect
}
+125
View File
@@ -0,0 +1,125 @@
package auth
import (
"log"
"net/http"
"net/url"
"strings"
"time"
)
func IsAuth(r *http.Request) (bool, error) {
jwt, err := getAuthToken(r)
if err != nil {
return false, err
}
_, err = ValidateJWT(jwt)
if err != nil {
return false, err
}
return true, nil
}
func LogoutEndpoint(w http.ResponseWriter, r *http.Request) {
setAuthCookie(w, r, "")
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func AuthMiddleware(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jwt, err := getAuthToken(r)
if err != nil {
log.Println("Error during token retrieval", err)
Redirect(w, r)
return
}
user, err := ValidateJWT(jwt)
if err != nil {
log.Println("Error during authorization", err)
Redirect(w, r)
return
}
log.Printf("%s: Authorized user %s", r.URL.Path, user)
w.Header().Add("user", user)
next.ServeHTTP(w, r)
})
}
func CallbackHandler(mux *http.ServeMux) {
url, err := url.Parse(config.RedirectURL)
if err != nil {
log.Fatalf("Invalid Redirect URL Submitted: %s\n", config.RedirectURL)
}
mux.HandleFunc(url.Path, func(w http.ResponseWriter, r *http.Request) {
if err := CheckState(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
code := r.URL.Query().Get("code")
idtoken, err := GetToken(code)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := CheckAudience(idtoken); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := CheckNonce(r, idtoken); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
jwt, err := NewJWT(idtoken.Subject)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
setAuthCookie(w, r, jwt)
redirect := GetRedirect(r.URL.Query().Get("state"))
log.Printf("DEBUG: REDIRECT %s\n", redirect)
http.Redirect(w, r, redirect, http.StatusFound)
})
}
func setAuthCookie(w http.ResponseWriter, r *http.Request, jwt string) {
c := &http.Cookie{
Name: authCookie,
Value: jwt,
MaxAge: int(time.Hour.Seconds()),
Secure: r.TLS != nil,
Path: "/",
HttpOnly: true,
}
http.SetCookie(w, c)
}
func getAuthToken(r *http.Request) (string, error) {
header := strings.Split(r.Header.Get("Authorization"), " ")
if len(header) > 1 {
token := header[1]
if len(strings.TrimSpace(token)) > 0 {
log.Printf("Got Auth Header %s\n", header[1])
return token, nil
}
}
cookie, err := r.Cookie(authCookie)
if err != nil {
return "", err
}
return cookie.Value, nil
}