OpenID authentification (#170)
* openid authentification * openid middleware * openId login * tidied login page * removed useless email text field
This commit is contained in:
@@ -22,6 +22,12 @@ type Config struct {
|
||||
QueueSize int `yaml:"queue_size"`
|
||||
SessionFilePath string `yaml:"session_file_path"`
|
||||
path string
|
||||
|
||||
UseOpenId bool `yaml:"use_openid"`
|
||||
OpenIdProviderURL string `yaml:"openid_provider_url"`
|
||||
OpenIdClientId string `yaml:"openid_client_id"`
|
||||
OpenIdClientSecret string `yaml:"openid_client_secret"`
|
||||
OpenIdRedirectURL string `yaml:"openid_redirect_url"`
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
||||
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
@@ -77,6 +78,9 @@ func ApplyRouter() func(chi.Router) {
|
||||
if config.Instance().RequireAuth {
|
||||
r.Use(middlewares.Authenticated)
|
||||
}
|
||||
if config.Instance().UseOpenId {
|
||||
r.Use(openid.Middleware)
|
||||
}
|
||||
r.Get("/ws", webSocket)
|
||||
r.Get("/sse", sse)
|
||||
}
|
||||
|
||||
172
server/openid/handler.go
Normal file
172
server/openid/handler.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package openid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/google/uuid"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type OAuth2SuccessResponse struct {
|
||||
OAuth2Token *oauth2.Token
|
||||
OAuth2RawToken string
|
||||
IDTokenClaims *json.RawMessage
|
||||
}
|
||||
|
||||
var (
|
||||
oauth2Config oauth2.Config
|
||||
verifier *oidc.IDTokenVerifier
|
||||
)
|
||||
|
||||
func Configure() {
|
||||
provider, err := oidc.NewProvider(context.Background(), config.Instance().OpenIdProviderURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
oauth2Config = oauth2.Config{
|
||||
ClientID: config.Instance().OpenIdClientId,
|
||||
ClientSecret: config.Instance().OpenIdClientSecret,
|
||||
RedirectURL: config.Instance().OpenIdRedirectURL,
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
}
|
||||
|
||||
verifier = provider.Verifier(&oidc.Config{
|
||||
ClientID: config.Instance().OpenIdClientId,
|
||||
})
|
||||
}
|
||||
|
||||
func Login(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
state = uuid.NewString()
|
||||
nonce = uuid.NewString() // maybe something cryptographycally more seucre?
|
||||
)
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "state",
|
||||
Value: state,
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
Secure: r.TLS != nil,
|
||||
Expires: time.Now().Add(time.Hour * 24 * 30),
|
||||
})
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "nonce",
|
||||
Value: nonce,
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
Secure: r.TLS != nil,
|
||||
Expires: time.Now().Add(time.Hour * 24 * 30),
|
||||
})
|
||||
|
||||
http.Redirect(w, r, oauth2Config.AuthCodeURL(state, oidc.Nonce(nonce)), http.StatusFound)
|
||||
}
|
||||
|
||||
func doAuthentification(r *http.Request) (*OAuth2SuccessResponse, error) {
|
||||
state, err := r.Cookie("state")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("state") != state.Value {
|
||||
return nil, errors.New("auth state does not match")
|
||||
}
|
||||
|
||||
oauth2Token, err := oauth2Config.Exchange(r.Context(), r.URL.Query().Get("code"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, errors.New("openid field \"id_token\" not found in oauth2 token")
|
||||
}
|
||||
|
||||
idToken, err := verifier.Verify(r.Context(), rawToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce, err := r.Cookie("nonce")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if idToken.Nonce != nonce.Value {
|
||||
return nil, errors.New("auth nonce does not match")
|
||||
}
|
||||
|
||||
// redact
|
||||
oauth2Token.AccessToken = ""
|
||||
|
||||
res := OAuth2SuccessResponse{
|
||||
oauth2Token,
|
||||
rawToken,
|
||||
&json.RawMessage{},
|
||||
}
|
||||
|
||||
if err := idToken.Claims(&res.IDTokenClaims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func SingIn(w http.ResponseWriter, r *http.Request) {
|
||||
res, err := doAuthentification(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "oid-token",
|
||||
Value: res.OAuth2RawToken,
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
Secure: r.TLS != nil,
|
||||
Expires: time.Now().Add(time.Hour * 24 * 30),
|
||||
})
|
||||
|
||||
// if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
|
||||
fmt.Fprintf(w, "Login succesfully, you may now close this window and refresh yt-dlp-webui.")
|
||||
}
|
||||
|
||||
func Logout(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "oid-token",
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
Secure: r.TLS != nil,
|
||||
Expires: time.Now(),
|
||||
})
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "state",
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
Secure: r.TLS != nil,
|
||||
Expires: time.Now(),
|
||||
})
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "nonce",
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
Secure: r.TLS != nil,
|
||||
Expires: time.Now(),
|
||||
})
|
||||
}
|
||||
20
server/openid/middleware.go
Normal file
20
server/openid/middleware.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package openid
|
||||
|
||||
import "net/http"
|
||||
|
||||
func Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token, err := r.Cookie("oid-token")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := verifier.Verify(r.Context(), token.Value); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
||||
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
|
||||
)
|
||||
|
||||
func Container(args *ContainerArgs) *Handler {
|
||||
@@ -21,6 +22,9 @@ func ApplyRouter(args *ContainerArgs) func(chi.Router) {
|
||||
if config.Instance().RequireAuth {
|
||||
r.Use(middlewares.Authenticated)
|
||||
}
|
||||
if config.Instance().UseOpenId {
|
||||
r.Use(openid.Middleware)
|
||||
}
|
||||
r.Post("/exec", h.Exec())
|
||||
r.Get("/running", h.Running())
|
||||
r.Get("/version", h.GetVersion())
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
||||
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
|
||||
)
|
||||
|
||||
// Dependency injection container.
|
||||
@@ -28,6 +29,9 @@ func ApplyRouter() func(chi.Router) {
|
||||
if config.Instance().RequireAuth {
|
||||
r.Use(middlewares.Authenticated)
|
||||
}
|
||||
if config.Instance().UseOpenId {
|
||||
r.Use(openid.Middleware)
|
||||
}
|
||||
r.Get("/ws", WebSocket)
|
||||
r.Post("/http", Post)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/logging"
|
||||
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
|
||||
"github.com/marcopeocchi/yt-dlp-web-ui/server/rest"
|
||||
ytdlpRPC "github.com/marcopeocchi/yt-dlp-web-ui/server/rpc"
|
||||
|
||||
@@ -168,6 +169,9 @@ func newServer(c serverConfig) *http.Server {
|
||||
if config.Instance().RequireAuth {
|
||||
r.Use(middlewares.Authenticated)
|
||||
}
|
||||
if config.Instance().UseOpenId {
|
||||
r.Use(openid.Middleware)
|
||||
}
|
||||
r.Post("/downloaded", handlers.ListDownloaded)
|
||||
r.Post("/delete", handlers.DeleteFile)
|
||||
r.Get("/d/{id}", handlers.DownloadFile)
|
||||
@@ -179,6 +183,12 @@ func newServer(c serverConfig) *http.Server {
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Post("/login", handlers.Login)
|
||||
r.Get("/logout", handlers.Logout)
|
||||
|
||||
r.Route("/openid", func(r chi.Router) {
|
||||
r.Get("/login", openid.Login)
|
||||
r.Get("/signin", openid.SingIn)
|
||||
r.Get("/logout", openid.Logout)
|
||||
})
|
||||
})
|
||||
|
||||
// RPC handlers
|
||||
|
||||
Reference in New Issue
Block a user