OpenID authentification (#170)

* openid authentification

* openid middleware

* openId login

* tidied login page

* removed useless email text field
This commit is contained in:
Marco Piovanello
2024-07-23 19:04:05 +02:00
committed by GitHub
parent 38a0cedc9c
commit 92aabc0086
11 changed files with 259 additions and 8 deletions

View File

@@ -6,19 +6,20 @@ import styled from '@emotion/styled'
import {
Button,
Container,
Divider,
Paper,
Stack,
TextField,
Typography
} from '@mui/material'
import { matchW } from 'fp-ts/lib/TaskEither'
import { pipe } from 'fp-ts/lib/function'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useRecoilValue } from 'recoil'
import { serverURL } from '../atoms/settings'
import { useToast } from '../hooks/toast'
import { ffetch } from '../lib/httpClient'
import { matchW } from 'fp-ts/lib/TaskEither'
import { pipe } from 'fp-ts/lib/function'
const LoginContainer = styled(Container)({
display: 'flex',
@@ -81,6 +82,8 @@ export default function Login() {
)()
}
const loginWithOpenId = () => window.open(`${url}/auth/openid/login`)
return (
<LoginContainer>
<Paper sx={{ padding: '1.5rem', minWidth: '25%' }}>
@@ -89,12 +92,8 @@ export default function Login() {
yt-dlp WebUI
</Title>
<Title fontWeight={'500'} fontSize={16} color={'gray'}>
Authentication token will expire after 30 days.
</Title>
<Title fontWeight={'500'} fontSize={16} color={'gray'}>
In order to enable RPC authentication append the --auth,
<br />
--user [username] and --pass [password] flags.
To configure authentication check the&nbsp;
<a href='https://github.com/marcopeocchi/yt-dlp-web-ui/wiki/Authentication-methods'>wiki</a>.
</Title>
<TextField
label="Username"
@@ -113,6 +112,16 @@ export default function Login() {
<Button variant="contained" size="large" onClick={() => login()}>
Submit
</Button>
<Divider>
<Typography color={'gray'}>
or use your authentication provider
</Typography>
</Divider>
<Button variant="contained" size="large" onClick={loginWithOpenId}>
Login with OpenID
</Button>
</Stack>
</Paper>
</LoginContainer>

5
go.mod
View File

@@ -4,12 +4,14 @@ go 1.22
require (
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/cors v1.2.1
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.1
github.com/reactivex/rxgo/v2 v2.5.0
golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.6.0
golang.org/x/sys v0.18.0
gopkg.in/yaml.v3 v3.0.1
@@ -25,11 +27,14 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pquerna/cachecontrol v0.2.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/teivah/onecontext v1.3.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.22.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
modernc.org/libc v1.47.0 // indirect
modernc.org/mathutil v1.6.0 // indirect

14
go.sum
View File

@@ -3,6 +3,8 @@ github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7h
github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -17,6 +19,8 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -38,6 +42,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k=
github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
github.com/reactivex/rxgo/v2 v2.5.0 h1:FhPgHwX9vKdNQB2gq9EPt+EKk9QrrzoeztGbEEnZam4=
github.com/reactivex/rxgo/v2 v2.5.0/go.mod h1:bs4fVZxcb5ZckLIOeIeVH942yunJLWDABWGbrHAW+qU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -47,6 +53,7 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/teivah/onecontext v0.0.0-20200513185103-40f981bfd775/go.mod h1:XUZ4x3oGhWfiOnUvTslnKKs39AWUct3g3yJvXTQSJOQ=
@@ -55,6 +62,8 @@ github.com/teivah/onecontext v1.3.0/go.mod h1:hoW1nmdPVK/0jrvGtcx8sCKYs2PiS4z0zz
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
@@ -63,6 +72,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
@@ -80,7 +91,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.19.5 h1:QlsZyQ1zf78DGeqnQ9ILi9hXyMdoC5e1qoGNUyBjHQw=

View File

@@ -11,6 +11,7 @@ import (
"github.com/marcopeocchi/yt-dlp-web-ui/server"
"github.com/marcopeocchi/yt-dlp-web-ui/server/cli"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"github.com/marcopeocchi/yt-dlp-web-ui/server/openid"
)
var (
@@ -91,6 +92,8 @@ func main() {
log.Println(cli.BgRed, "config", cli.Reset, err)
}
openid.Configure()
server.RunBlocking(&server.RunConfig{
Host: c.Host,
Port: c.Port,

View File

@@ -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 (

View File

@@ -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
View 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(),
})
}

View 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)
})
}

View File

@@ -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())

View File

@@ -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)
}

View File

@@ -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