@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user