Skip to content

Commit

Permalink
chore(auth): move files around
Browse files Browse the repository at this point in the history
  • Loading branch information
Darkness4 committed Jan 24, 2024
1 parent 464fff7 commit 90d5d3d
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 178 deletions.
118 changes: 0 additions & 118 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,130 +2,12 @@
package auth

import (
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/Darkness4/auth-htmx/jwt"
"github.com/gorilla/csrf"
"github.com/rs/zerolog/log"
)

// Auth is a service that provides HTTP handlers and middlewares used for authentication.
type Auth struct {
JWTSecret jwt.Secret
Providers map[string]Provider
}

// Login is the handler that redirect to the authentication page of the OAuth Provider.
func (a *Auth) Login() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
val, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
p := val.Get("provider")

provider, ok := a.Providers[strings.ToLower(p)]
if !ok {
http.Error(w, "auth provider not known", http.StatusUnauthorized)
return
}

token := csrf.Token(r)
cookie := &http.Cookie{
Name: "csrf_token",
Value: token,
Expires: time.Now().Add(1 * time.Minute), // Set expiration time as needed
HttpOnly: true,
}
// State contain the provider and the csrf token.
state := fmt.Sprintf("%s,%s", token, p)
http.SetCookie(w, cookie)
http.Redirect(
w,
r,
provider.AuthCodeURL(state),
http.StatusFound,
)
}
}

// CallBack is the handler called after login.
//
// It:
//
// 1. Fetches the accessToken
// 2. Fetches some user info and wrap them in a JWT token
// 3. Store the JWT token in a cookie for the browser.
func (a *Auth) CallBack() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
val, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
code := val.Get("code")
csrfToken, p, ok := strings.Cut(val.Get("state"), ",")
if !ok {
log.Error().Msg(fmt.Sprintf("invalid state: %s", val.Get("state")))
http.Error(
w,
fmt.Sprintf("invalid state: %s", val.Get("state")),
http.StatusInternalServerError,
)
return
}

expectedCSRF, err := r.Cookie("csrf_token")
if err == http.ErrNoCookie {
http.Error(w, "no csrf cookie error", http.StatusUnauthorized)
return
}
if csrfToken != expectedCSRF.Value {
http.Error(w, "csrf error", http.StatusUnauthorized)
return
}

provider, ok := a.Providers[strings.ToLower(p)]
if !ok {
http.Error(w, "auth provider not known", http.StatusUnauthorized)
return
}

oauth2Token, err := provider.Exchange(r.Context(), code)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

userID, userName, err := provider.GetIdentity(r.Context(), oauth2Token)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

token, err := a.JWTSecret.GenerateToken(userID, userName, strings.ToLower(p))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

cookie := &http.Cookie{
Name: jwt.TokenCookieKey,
Value: token,
Path: "/",
Expires: time.Now().Add(jwt.ExpiresDuration),
HttpOnly: true,
}
http.SetCookie(w, cookie)
http.Redirect(w, r, "/", http.StatusFound)
}
}

// Logout removes session cookies and redirect to home.
func Logout(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(jwt.TokenCookieKey)
Expand Down
59 changes: 9 additions & 50 deletions auth/auth_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,70 +5,29 @@ import (
"fmt"
"strings"

"github.com/Darkness4/auth-htmx/auth/oauth"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
)

// Config is the authentication configuration definition for the application.
type Config struct {
Providers []ProviderConfig `yaml:"providers"`
SelfHostUsers bool `yaml:"selfHostUsers"`
}

// ProviderConfig is the configuration of one provider to achieve the OAuth2 flow.
type ProviderConfig struct {
Type ProviderType `yaml:"type"`
Name string `yaml:"name"`
ClientID string `yaml:"clientID"`
ClientSecret string `yaml:"clientSecret"`
Endpoint string `yaml:"endpoint"`
}

// ProviderType is a string uses the indentify edge cases in authentication.
type ProviderType string

const (
// ProviderGitHub is the type of the authentication provider that uses GitHub OAuth2.
ProviderGitHub ProviderType = "github"
// ProviderOIDC is the generic type of authentication provider that uses OIDC.
ProviderOIDC ProviderType = "oidc"
)

// Provider is the interface that defines the necessary methods of authentication providers.
type Provider interface {
// AuthCodeURL returns the URL of the consent page that asks for permissions.
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
// Exchange converts a code into an OAuth2 token.
Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)

// DisplayName is the provider's name that can be displayed publicly.
DisplayName() string
GetIdentity(
ctx context.Context,
token *oauth2.Token,
) (userID string, userName string, err error)
}

// OIDCClaims are the standard fields given by an OIDC provider.
type OIDCClaims struct {
jwt.RegisteredClaims
Name string `json:"name"`
Email string `json:"email"`
Providers []oauth.ProviderConfig `yaml:"providers"`
SelfHostUsers bool `yaml:"selfHostUsers"`
}

// GenerateProviders generates a map of provider based on the given configuration.
func GenerateProviders(
ctx context.Context,
config Config,
redirectURL string,
) (pp map[string]Provider, err error) {
pp = make(map[string]Provider)
) (pp map[string]oauth.Provider, err error) {
pp = make(map[string]oauth.Provider)
for _, p := range config.Providers {
switch p.Type {
case ProviderGitHub:
pp[strings.ToLower(p.Name)] = &GitHubProvider{
case oauth.ProviderGitHub:
pp[strings.ToLower(p.Name)] = &oauth.GitHubProvider{
Name: p.Name,
Config: &oauth2.Config{
ClientID: p.ClientID,
Expand All @@ -78,12 +37,12 @@ func GenerateProviders(
Scopes: []string{"read:user", "user:email"},
},
}
case ProviderOIDC:
case oauth.ProviderOIDC:
provider, err := oidc.NewProvider(ctx, p.Endpoint)
if err != nil {
return pp, err
}
pp[strings.ToLower(p.Name)] = &OIDCProvider{
pp[strings.ToLower(p.Name)] = &oauth.OIDCProvider{
Name: p.Name,
Config: &oauth2.Config{
ClientID: p.ClientID,
Expand Down
127 changes: 127 additions & 0 deletions auth/oauth/oauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Package auth defines the authentication layer of the application.
package oauth

import (
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/Darkness4/auth-htmx/jwt"
"github.com/gorilla/csrf"
"github.com/rs/zerolog/log"
)

// OAuth is a service that provides HTTP handlers and middlewares used for authentication.
type OAuth struct {
JWTSecret jwt.Secret
Providers map[string]Provider
}

// Login is the handler that redirect to the authentication page of the OAuth Provider.
func (a *OAuth) Login() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
val, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
p := val.Get("provider")

provider, ok := a.Providers[strings.ToLower(p)]
if !ok {
http.Error(w, "auth provider not known", http.StatusUnauthorized)
return
}

token := csrf.Token(r)
cookie := &http.Cookie{
Name: "csrf_token",
Value: token,
Expires: time.Now().Add(1 * time.Minute), // Set expiration time as needed
HttpOnly: true,
}
// State contain the provider and the csrf token.
state := fmt.Sprintf("%s,%s", token, p)
http.SetCookie(w, cookie)
http.Redirect(
w,
r,
provider.AuthCodeURL(state),
http.StatusFound,
)
}
}

// CallBack is the handler called after login.
//
// It:
//
// 1. Fetches the accessToken
// 2. Fetches some user info and wrap them in a JWT token
// 3. Store the JWT token in a cookie for the browser.
func (a *OAuth) CallBack() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
val, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
code := val.Get("code")
csrfToken, p, ok := strings.Cut(val.Get("state"), ",")
if !ok {
log.Error().Msg(fmt.Sprintf("invalid state: %s", val.Get("state")))
http.Error(
w,
fmt.Sprintf("invalid state: %s", val.Get("state")),
http.StatusInternalServerError,
)
return
}

expectedCSRF, err := r.Cookie("csrf_token")
if err == http.ErrNoCookie {
http.Error(w, "no csrf cookie error", http.StatusUnauthorized)
return
}
if csrfToken != expectedCSRF.Value {
http.Error(w, "csrf error", http.StatusUnauthorized)
return
}

provider, ok := a.Providers[strings.ToLower(p)]
if !ok {
http.Error(w, "auth provider not known", http.StatusUnauthorized)
return
}

oauth2Token, err := provider.Exchange(r.Context(), code)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

userID, userName, err := provider.GetIdentity(r.Context(), oauth2Token)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

token, err := a.JWTSecret.GenerateToken(userID, userName, strings.ToLower(p))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

cookie := &http.Cookie{
Name: jwt.TokenCookieKey,
Value: token,
Path: "/",
Expires: time.Now().Add(jwt.ExpiresDuration),
HttpOnly: true,
}
http.SetCookie(w, cookie)
http.Redirect(w, r, "/", http.StatusFound)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package auth
package oauth

import (
"context"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package auth
package oauth

import (
"context"
Expand Down
Loading

0 comments on commit 90d5d3d

Please sign in to comment.