247 lines
5.9 KiB
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,
|
|
}
|
|
}
|