Skip to content

Commit 4fbb589

Browse files
committed
implement oidc
Signed-off-by: bakito <github@bakito.ch>
1 parent 585ea34 commit 4fbb589

18 files changed

Lines changed: 2100 additions & 23 deletions

File tree

docs/Configuration.md

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,33 @@ Most configuration can be set directly using environment variables or flags. The
2727

2828
### HTTP configuration variables
2929

30-
| Environment variable | Default | Required | Description |
31-
| ------------------------------------------ | ------- | -------- | ----------------------------------------------------- |
32-
| `SHIORI_HTTP_ENABLED` | True | No | Enable HTTP service |
33-
| `SHIORI_HTTP_PORT` | 8080 | No | Port number for the HTTP service |
34-
| `SHIORI_HTTP_ADDRESS` | : | No | Address for the HTTP service |
35-
| `SHIORI_HTTP_ROOT_PATH` | / | No | Root path for the HTTP service |
36-
| `SHIORI_HTTP_ACCESS_LOG` | True | No | Logging accessibility for HTTP requests |
37-
| `SHIORI_HTTP_SERVE_WEB_UI` | True | No | Serving Web UI via HTTP. Disable serves only the API. |
38-
| `SHIORI_HTTP_SECRET_KEY` | | **Yes** | Secret key for HTTP sessions. |
39-
| `SHIORI_HTTP_BODY_LIMIT` | 1024 | No | Limit for request body size |
40-
| `SHIORI_HTTP_READ_TIMEOUT` | 10s | No | Maximum duration for reading the entire request |
41-
| `SHIORI_HTTP_WRITE_TIMEOUT` | 10s | No | Maximum duration before timing out writes |
42-
| `SHIORI_HTTP_IDLE_TIMEOUT` | 10s | No | Maximum amount of time to wait for the next request |
43-
| `SHIORI_HTTP_DISABLE_KEEP_ALIVE` | true | No | Disable HTTP keep-alive connections |
44-
| `SHIORI_HTTP_DISABLE_PARSE_MULTIPART_FORM` | true | No | Disable pre-parsing of multipart form |
45-
| `SHIORI_SSO_PROXY_AUTH_ENABLED` | false | No | Enable SSO Auth Proxy Header |
46-
| `SHIORI_SSO_PROXY_AUTH_HEADER_NAME` | Remote-User | No | List of CIDRs of trusted proxies |
47-
| `SHIORI_SSO_PROXY_AUTH_TRUSTED` | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7 | No | List of CIDRs of trusted proxies |
30+
| Environment variable | Default | Required | Description |
31+
|--------------------------------------------|-----------------------------------------------------|----------|-------------------------------------------------------|
32+
| `SHIORI_HTTP_ENABLED` | True | No | Enable HTTP service |
33+
| `SHIORI_HTTP_PORT` | 8080 | No | Port number for the HTTP service |
34+
| `SHIORI_HTTP_ADDRESS` | : | No | Address for the HTTP service |
35+
| `SHIORI_HTTP_ROOT_PATH` | / | No | Root path for the HTTP service |
36+
| `SHIORI_HTTP_ACCESS_LOG` | True | No | Logging accessibility for HTTP requests |
37+
| `SHIORI_HTTP_SERVE_WEB_UI` | True | No | Serving Web UI via HTTP. Disable serves only the API. |
38+
| `SHIORI_HTTP_SECRET_KEY` | | **Yes** | Secret key for HTTP sessions. |
39+
| `SHIORI_HTTP_BODY_LIMIT` | 1024 | No | Limit for request body size |
40+
| `SHIORI_HTTP_READ_TIMEOUT` | 10s | No | Maximum duration for reading the entire request |
41+
| `SHIORI_HTTP_WRITE_TIMEOUT` | 10s | No | Maximum duration before timing out writes |
42+
| `SHIORI_HTTP_IDLE_TIMEOUT` | 10s | No | Maximum amount of time to wait for the next request |
43+
| `SHIORI_HTTP_DISABLE_KEEP_ALIVE` | true | No | Disable HTTP keep-alive connections |
44+
| `SHIORI_HTTP_DISABLE_PARSE_MULTIPART_FORM` | true | No | Disable pre-parsing of multipart form |
45+
| `SHIORI_SSO_PROXY_AUTH_ENABLED` | false | No | Enable SSO Auth Proxy Header |
46+
| `SHIORI_SSO_PROXY_AUTH_HEADER_NAME` | Remote-User | No | List of CIDRs of trusted proxies |
47+
| `SHIORI_SSO_PROXY_AUTH_TRUSTED` | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7 | No | List of CIDRs of trusted proxies |
48+
| `SHIORI_OIDC_ENABLED` | false | No | Enable OIDC Authentication |
49+
| `SHIORI_OIDC_ISSUER` | | No | OIDC Issuer URL |
50+
| `SHIORI_OIDC_CLIENT_ID` | | No | OIDC Client ID |
51+
| `SHIORI_OIDC_CLIENT_SECRET` | | No | OIDC Client Secret |
52+
| `SHIORI_OIDC_REDIRECT_URL` | | No | OIDC Redirect URL |
53+
| `SHIORI_OIDC_SCOPES` | openid,profile,email | No | OIDC Scopes (comma separated) |
54+
| `SHIORI_OIDC_USERNAME_CLAIM` | preferred_username | No | OIDC Username Claim |
55+
| `SHIORI_OIDC_AUTO_REGISTER` | false | No | Automatically register new OIDC users |
56+
| `SHIORI_OIDC_PROVIDER_NAME` | OIDC | No | Name of the OIDC Provider |
4857

4958
### Storage Configuration
5059

docs/swagger/docs.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,26 @@ const docTemplate = `{
189189
}
190190
}
191191
},
192+
"/api/v1/auth/config": {
193+
"get": {
194+
"description": "Get authentication configuration like OIDC status",
195+
"produces": [
196+
"application/json"
197+
],
198+
"tags": [
199+
"Auth"
200+
],
201+
"summary": "Get authentication configuration",
202+
"responses": {
203+
"200": {
204+
"description": "OK",
205+
"schema": {
206+
"$ref": "#/definitions/api_v1.AuthConfigResponse"
207+
}
208+
}
209+
}
210+
}
211+
},
192212
"/api/v1/auth/login": {
193213
"post": {
194214
"consumes": [
@@ -265,6 +285,24 @@ const docTemplate = `{
265285
}
266286
}
267287
},
288+
"/api/v1/auth/oidc/callback": {
289+
"get": {
290+
"tags": [
291+
"Auth"
292+
],
293+
"summary": "OIDC callback URL",
294+
"responses": {}
295+
}
296+
},
297+
"/api/v1/auth/oidc/login": {
298+
"get": {
299+
"tags": [
300+
"Auth"
301+
],
302+
"summary": "Redirect to OIDC provider for login",
303+
"responses": {}
304+
}
305+
},
268306
"/api/v1/auth/refresh": {
269307
"post": {
270308
"produces": [
@@ -731,6 +769,17 @@ const docTemplate = `{
731769
}
732770
},
733771
"definitions": {
772+
"api_v1.AuthConfigResponse": {
773+
"type": "object",
774+
"properties": {
775+
"oidc_enabled": {
776+
"type": "boolean"
777+
},
778+
"oidc_provider_name": {
779+
"type": "string"
780+
}
781+
}
782+
},
734783
"api_v1.bookmarkTagPayload": {
735784
"type": "object",
736785
"required": [

docs/swagger/swagger.json

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,26 @@
178178
}
179179
}
180180
},
181+
"/api/v1/auth/config": {
182+
"get": {
183+
"description": "Get authentication configuration like OIDC status",
184+
"produces": [
185+
"application/json"
186+
],
187+
"tags": [
188+
"Auth"
189+
],
190+
"summary": "Get authentication configuration",
191+
"responses": {
192+
"200": {
193+
"description": "OK",
194+
"schema": {
195+
"$ref": "#/definitions/api_v1.AuthConfigResponse"
196+
}
197+
}
198+
}
199+
}
200+
},
181201
"/api/v1/auth/login": {
182202
"post": {
183203
"consumes": [
@@ -254,6 +274,24 @@
254274
}
255275
}
256276
},
277+
"/api/v1/auth/oidc/callback": {
278+
"get": {
279+
"tags": [
280+
"Auth"
281+
],
282+
"summary": "OIDC callback URL",
283+
"responses": {}
284+
}
285+
},
286+
"/api/v1/auth/oidc/login": {
287+
"get": {
288+
"tags": [
289+
"Auth"
290+
],
291+
"summary": "Redirect to OIDC provider for login",
292+
"responses": {}
293+
}
294+
},
257295
"/api/v1/auth/refresh": {
258296
"post": {
259297
"produces": [
@@ -720,6 +758,17 @@
720758
}
721759
},
722760
"definitions": {
761+
"api_v1.AuthConfigResponse": {
762+
"type": "object",
763+
"properties": {
764+
"oidc_enabled": {
765+
"type": "boolean"
766+
},
767+
"oidc_provider_name": {
768+
"type": "string"
769+
}
770+
}
771+
},
723772
"api_v1.bookmarkTagPayload": {
724773
"type": "object",
725774
"required": [

docs/swagger/swagger.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
definitions:
2+
api_v1.AuthConfigResponse:
3+
properties:
4+
oidc_enabled:
5+
type: boolean
6+
oidc_provider_name:
7+
type: string
8+
type: object
29
api_v1.bookmarkTagPayload:
310
properties:
411
tag_id:
@@ -305,6 +312,19 @@ paths:
305312
summary: Update account information
306313
tags:
307314
- Auth
315+
/api/v1/auth/config:
316+
get:
317+
description: Get authentication configuration like OIDC status
318+
produces:
319+
- application/json
320+
responses:
321+
"200":
322+
description: OK
323+
schema:
324+
$ref: '#/definitions/api_v1.AuthConfigResponse'
325+
summary: Get authentication configuration
326+
tags:
327+
- Auth
308328
/api/v1/auth/login:
309329
post:
310330
consumes:
@@ -353,6 +373,18 @@ paths:
353373
summary: Get information for the current logged in user
354374
tags:
355375
- Auth
376+
/api/v1/auth/oidc/callback:
377+
get:
378+
responses: {}
379+
summary: OIDC callback URL
380+
tags:
381+
- Auth
382+
/api/v1/auth/oidc/login:
383+
get:
384+
responses: {}
385+
summary: Redirect to OIDC provider for login
386+
tags:
387+
- Auth
356388
/api/v1/auth/refresh:
357389
post:
358390
produces:

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
git.sr.ht/~emersion/go-sqlite3-fts5 v0.0.0-20250706113457-213d0e8755e5
88
github.com/PuerkitoBio/goquery v1.10.3
99
github.com/blang/semver v3.5.1+incompatible
10+
github.com/coreos/go-oidc/v3 v3.17.0
1011
github.com/disintegration/imaging v1.6.2
1112
github.com/fatih/color v1.18.0
1213
github.com/go-shiori/go-epub v1.2.2-0.20241010194245-bd691046db94
@@ -37,6 +38,7 @@ require (
3738
golang.org/x/crypto v0.42.0
3839
golang.org/x/image v0.31.0
3940
golang.org/x/net v0.44.0
41+
golang.org/x/oauth2 v0.35.0
4042
golang.org/x/term v0.35.0
4143
modernc.org/sqlite v1.39.0
4244
)
@@ -66,6 +68,7 @@ require (
6668
github.com/felixge/httpsnoop v1.0.4 // indirect
6769
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
6870
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
71+
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
6972
github.com/go-logr/logr v1.4.3 // indirect
7073
github.com/go-logr/stdr v1.2.2 // indirect
7174
github.com/go-ole/go-ole v1.3.0 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
3232
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
3333
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
3434
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
35+
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
36+
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
3537
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
3638
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
3739
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -64,6 +66,8 @@ github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIp
6466
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
6567
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
6668
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
69+
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
70+
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
6771
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
6872
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
6973
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -339,6 +343,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
339343
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
340344
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
341345
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
346+
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
347+
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
342348
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
343349
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
344350
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

internal/config/config.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ type HttpConfig struct {
6969
SSOProxyAuth bool `env:"SSO_PROXY_AUTH_ENABLED,default=false"`
7070
SSOProxyAuthHeaderName string `env:"SSO_PROXY_AUTH_HEADER_NAME,default=Remote-User"`
7171
SSOProxyAuthTrusted []string `env:"SSO_PROXY_AUTH_TRUSTED,default=10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7"`
72+
73+
OIDCEnabled bool `env:"OIDC_ENABLED,default=false"`
74+
OIDCIssuer string `env:"OIDC_ISSUER"`
75+
OIDCClientID string `env:"OIDC_CLIENT_ID"`
76+
OIDCClientSecret string `env:"OIDC_CLIENT_SECRET"`
77+
OIDCRedirectURL string `env:"OIDC_REDIRECT_URL"`
78+
OIDCScopes string `env:"OIDC_SCOPES,default=openid,profile,email"`
79+
OIDCUsernameClaim string `env:"OIDC_USERNAME_CLAIM,default=preferred_username"`
80+
OIDCAutoRegister bool `env:"OIDC_AUTO_REGISTER,default=false"`
81+
OIDCProviderName string `env:"OIDC_PROVIDER_NAME,default=OIDC"`
7282
}
7383

7484
// SetDefaults sets the default values for the configuration
@@ -159,13 +169,37 @@ func (c *Config) DebugConfiguration(logger *logrus.Logger) {
159169
logger.Debugf(" SHIORI_SSO_PROXY_AUTH_ENABLED: %t", c.Http.SSOProxyAuth)
160170
logger.Debugf(" SHIORI_SSO_PROXY_AUTH_HEADER_NAME: %s", c.Http.SSOProxyAuthHeaderName)
161171
logger.Debugf(" SHIORI_SSO_PROXY_AUTH_TRUSTED: %v", c.Http.SSOProxyAuthTrusted)
172+
logger.Debugf(" SHIORI_OIDC_ENABLED: %t", c.Http.OIDCEnabled)
173+
logger.Debugf(" SHIORI_OIDC_ISSUER: %s", c.Http.OIDCIssuer)
174+
logger.Debugf(" SHIORI_OIDC_CLIENT_ID: %s", c.Http.OIDCClientID)
175+
logger.Debugf(" SHIORI_OIDC_CLIENT_SECRET: %d characters", len(c.Http.OIDCClientSecret))
176+
logger.Debugf(" SHIORI_OIDC_REDIRECT_URL: %s", c.Http.OIDCRedirectURL)
177+
logger.Debugf(" SHIORI_OIDC_SCOPES: %s", c.Http.OIDCScopes)
178+
logger.Debugf(" SHIORI_OIDC_USERNAME_CLAIM: %s", c.Http.OIDCUsernameClaim)
179+
logger.Debugf(" SHIORI_OIDC_AUTO_REGISTER: %t", c.Http.OIDCAutoRegister)
180+
logger.Debugf(" SHIORI_OIDC_PROVIDER_NAME: %s", c.Http.OIDCProviderName)
162181
}
163182

164183
func (c *Config) IsValid() error {
165184
if err := c.Http.IsValid(); err != nil {
166185
return fmt.Errorf("http configuration is invalid: %w", err)
167186
}
168187

188+
if c.Http.OIDCEnabled {
189+
if c.Http.OIDCIssuer == "" {
190+
return fmt.Errorf("OIDC issuer is required when OIDC is enabled")
191+
}
192+
if c.Http.OIDCClientID == "" {
193+
return fmt.Errorf("OIDC client ID is required when OIDC is enabled")
194+
}
195+
if c.Http.OIDCClientSecret == "" {
196+
return fmt.Errorf("OIDC client secret is required when OIDC is enabled")
197+
}
198+
if c.Http.OIDCRedirectURL == "" {
199+
return fmt.Errorf("OIDC redirect URL is required when OIDC is enabled")
200+
}
201+
}
202+
169203
return nil
170204
}
171205

internal/config/config_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77

88
"github.com/sirupsen/logrus"
9+
"github.com/stretchr/testify/assert"
910
"github.com/stretchr/testify/require"
1011
)
1112

@@ -116,4 +117,24 @@ func TestConfigIsValid(t *testing.T) {
116117
cfg.Http.RootPath = "/invalid"
117118
require.Error(t, cfg.IsValid())
118119
})
120+
121+
t.Run("verify oidc params", func(t *testing.T) {
122+
cfg := ParseServerConfiguration(context.TODO(), log)
123+
cfg.Http.OIDCEnabled = true
124+
assert.Error(t, cfg.IsValid())
125+
cfg.Http.OIDCIssuer = "http://issuer"
126+
assert.Error(t, cfg.IsValid())
127+
cfg.Http.OIDCClientID = "client-id"
128+
assert.Error(t, cfg.IsValid())
129+
cfg.Http.OIDCClientSecret = "client-secret"
130+
assert.Error(t, cfg.IsValid())
131+
cfg.Http.OIDCRedirectURL = "http://redirect.url"
132+
require.NoError(t, cfg.IsValid())
133+
})
134+
}
135+
136+
func TestDebugConfiguration(t *testing.T) {
137+
log := logrus.New()
138+
cfg := ParseServerConfiguration(context.TODO(), log)
139+
cfg.DebugConfiguration(log)
119140
}

0 commit comments

Comments
 (0)