glance/internal/glance/auth.go

247 lines
5.9 KiB
Go

package glance
import (
"context"
"errors"
"log/slog"
"net/http"
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
type GlanceAuth struct {
OidcProvider *oidc.Provider
oAuth2Config oauth2.Config
OidcVefifier *oidc.IDTokenVerifier
cookieName string
https bool
}
type AuthenticatationResult struct {
IDToken string
RefreshToken string
IDTokenClaims *Claims
}
type Claims struct {
Issuer string `json:"iss"`
Audience string `json:"aud"`
IssuedAt UnixTime `json:"iat"`
Expiration UnixTime `json:"exp"`
}
func CreateGlanceAuth(ctx context.Context, config *config) (*GlanceAuth, error) {
provider, err := oidc.NewProvider(ctx, config.Authentication.Issuer)
if err != nil {
return nil, err
}
verifier := provider.Verifier(&oidc.Config{
ClientID: config.Authentication.ClientId,
})
scopes := []string{oidc.ScopeOpenID}
scopes = append(scopes, strings.Split(config.Authentication.Scopes, " ")...)
return &GlanceAuth{
OidcProvider: provider,
oAuth2Config: oauth2.Config{
ClientID: config.Authentication.ClientId,
ClientSecret: config.Authentication.ClientSecret,
RedirectURL: config.Server.BaseURL + "/auth/resp",
// Discovery returns the OAuth2 endpoints.
Endpoint: provider.Endpoint(),
// "openid" is a required scope for OpenID Connect flows.
Scopes: scopes,
},
OidcVefifier: verifier,
cookieName: config.Authentication.CookieName,
https: strings.HasPrefix(config.Server.BaseURL, "https://") || config.Server.UsesHttps,
}, nil
}
func (fw *GlanceAuth) VerifyToken(ctx context.Context, oauth2Token *oauth2.Token) (AuthenticatationResult, error) {
var result AuthenticatationResult
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
return result, errors.New("No id_token field in oauth2 token")
}
idToken, err := fw.OidcVefifier.Verify(ctx, rawIDToken)
if err != nil {
return result, err
}
result = AuthenticatationResult{
IDToken: rawIDToken,
RefreshToken: oauth2Token.RefreshToken,
IDTokenClaims: new(Claims),
}
if err := idToken.Claims(&result.IDTokenClaims); err != nil {
return result, err
}
return result, nil
}
func (fw *GlanceAuth) RefreshToken(ctx context.Context, refreshToken string) (*AuthenticatationResult, error) {
var result AuthenticatationResult
tokenSource := fw.oAuth2Config.TokenSource(ctx, &oauth2.Token{RefreshToken: refreshToken})
oauth2Token, err := tokenSource.Token()
if err != nil {
return &result, err
}
result, err = fw.VerifyToken(ctx, oauth2Token)
if err != nil {
return &result, err
}
return &result, nil
}
func getBaseCookie() *http.Cookie {
return &http.Cookie{
Path: "/",
HttpOnly: true,
Secure: true,
}
}
func (fw *GlanceAuth) MakeAuthCookie(authResult *AuthenticatationResult) *http.Cookie {
cookie := getBaseCookie()
cookie.Name = fw.cookieName
cookie.Value = authResult.IDToken
cookie.Expires = time.Now().Local().Add(time.Hour * 24)
return cookie
}
func (fw *GlanceAuth) GetAuthCookie(r *http.Request) (*http.Cookie, error) {
return r.Cookie(fw.cookieName)
}
func (fw *GlanceAuth) ClearAuthCookie() *http.Cookie {
cookie := getBaseCookie()
cookie.Name = fw.cookieName
cookie.Expires = time.Now().Local().Add(time.Hour * -1)
return cookie
}
func (fw *GlanceAuth) MakeRefreshAuthCookie(authResult *AuthenticatationResult) *http.Cookie {
cookie := getBaseCookie()
cookie.Name = fw.cookieName + "-refresh"
cookie.Value = authResult.RefreshToken
cookie.Expires = time.Now().Local().Add(time.Hour * 24)
return cookie
}
func (fw *GlanceAuth) GetRefreshAuthCookie(r *http.Request) (*http.Cookie, error) {
return r.Cookie(fw.cookieName + "-refresh")
}
func (fw *GlanceAuth) ClearRefreshAuthCookie() *http.Cookie {
cookie := getBaseCookie()
cookie.Name = fw.cookieName + "-refresh"
cookie.Expires = time.Now().Local().Add(time.Hour * -1)
return cookie
}
func (fw *GlanceAuth) HandleAuthentication(ctx context.Context, code string, host string) (*AuthenticatationResult, error) {
var result AuthenticatationResult
oauth := fw.OAuthConfig(host)
oauth2Token, err := oauth.Exchange(ctx, code)
if err != nil {
slog.Error(err.Error())
return &result, err
}
result, err = fw.VerifyToken(ctx, oauth2Token)
if err != nil {
slog.Error(err.Error())
return &result, err
}
slog.Debug("Authentication was succesful.")
return &result, nil
}
func (fw *GlanceAuth) IsAuthenticated(context context.Context, w http.ResponseWriter, r *http.Request) (*Claims, error) {
var claims Claims
// Check if we have an Auth cookie
cookie, err := fw.GetAuthCookie(r)
if err != nil {
return &claims, err
}
// check if the token is valid
idToken, err := fw.OidcVefifier.Verify(context, cookie.Value)
switch {
case err == nil: // Token is valid
slog.Debug("Received valid token.")
claims = Claims{}
if err := idToken.Claims(&claims); err != nil {
slog.Error(err.Error())
return &claims, err
}
return &claims, nil
case strings.Contains(err.Error(), "expired"): // Token is expired
slog.Debug("Received expired token, trying to refesh it.")
refreshCookie, err := fw.GetRefreshAuthCookie(r)
if err != nil {
slog.Error(err.Error())
return &claims, err
}
result, err := fw.RefreshToken(context, refreshCookie.Value)
if err != nil {
slog.Error(err.Error())
return &claims, err
}
http.SetCookie(w, fw.MakeAuthCookie(result))
if len(result.RefreshToken) > 0 { // Do we have an refresh token?
http.SetCookie(w, fw.MakeRefreshAuthCookie(result))
}
return result.IDTokenClaims, nil
default:
slog.Error(err.Error())
return &claims, err
}
}
func (fw *GlanceAuth) OAuthConfig(host string) oauth2.Config {
var proto string
if fw.https {
proto = "https://"
} else {
proto = "http://"
}
return oauth2.Config{
ClientID: fw.oAuth2Config.ClientID,
ClientSecret: fw.oAuth2Config.ClientSecret,
RedirectURL: proto + host + "/auth/resp",
Endpoint: fw.oAuth2Config.Endpoint,
Scopes: fw.oAuth2Config.Scopes,
}
}