Implement OIDC auth (#98)

This commit is contained in:
Thomas Miceli 2023-09-25 13:08:06 +02:00
commit 35297a287a
No known key found for this signature in database
GPG key ID: D86C6F6390AF050F
9 changed files with 99 additions and 9 deletions

View file

@ -30,7 +30,7 @@ A self-hosted pastebin **powered by Git**. [Try it here](https://opengist.thomic
* Search for snippets ; browse users snippets, likes and forks
* Editor with indentation mode & size ; drag and drop files
* Download raw files or as a ZIP archive
* OAuth2 login with GitHub and Gitea
* OAuth2 login with GitHub, Gitea, and OpenID Connect
* Avatars via Gravatar or OAuth2 providers
* Light/Dark mode
* Responsive UI
@ -113,8 +113,8 @@ You would only need to specify the configuration options you want to change —
<details>
<summary>Configuration option list</summary>
| YAML Config Key | Environment Variable | Default value | Description |
|-----------------------|--------------------------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| YAML Config Key | Environment Variable | Default value | Description |
| --------------------- | ------------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. |
| external-url | OG_EXTERNAL_URL | none | Public URL for the Git HTTP/SSH connection. If not set, uses the URL from the request. |
| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. |
@ -133,6 +133,9 @@ You would only need to specify the configuration options you want to change —
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. |
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. |
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. |
</details>
@ -221,7 +224,7 @@ service fail2ban restart
## Configure OAuth
Opengist can be configured to use OAuth to authenticate users, with GitHub or Gitea.
Opengist can be configured to use OAuth to authenticate users, with GitHub, Gitea, or OpenID Connect.
<details>
<summary>Integrate Github</summary>
@ -249,6 +252,20 @@ Opengist can be configured to use OAuth to authenticate users, with GitHub or Gi
```
</details>
<details>
<summary>Integrate OpenID</summary>
* Add a new OAuth app in Application settings of your OIDC provider
* Set 'Redirect URI' to `http://opengist.domain/oauth/openid-connect/callback`
* Copy the 'Client ID', 'Client Secret', and the discovery endpoint, and add them to the configuration :
```yaml
oidc.client-key: <key>
oidc.secret: <secret>
# Discovery endpoint of the OpenID provider
oidc.discovery-url: http://auth.example.com/.well-known/openid-configuration
```
</details>
## License
Opengist is licensed under the [AGPL-3.0 license](LICENSE).

View file

@ -51,7 +51,7 @@ ssh.keygen-executable: ssh-keygen
# OAuth2 configuration
# The callback/redirect URL must be http://opengist.domain/oauth/<github|gitea>/callback
# The callback/redirect URL must be http://opengist.domain/oauth/<github|gitea|openid-connect>/callback
# To create a new OAuth2 application using GitHub : https://github.com/settings/applications/new
github.client-key:
@ -62,3 +62,9 @@ gitea.client-key:
gitea.secret:
# URL of the Gitea instance. Default: https://gitea.com/
gitea.url: https://gitea.com/
# To create a new OAuth2 application using OpenID Connect:
oidc.client-key:
oidc.secret:
# Discovery endpoint of the OpenID provider
oidc.discovery-url:

View file

@ -45,6 +45,10 @@ type config struct {
GiteaClientKey string `yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY"`
GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"`
GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"`
OIDCClientKey string `yaml:"oidc.client-key" env:"OG_OIDC_CLIENT_KEY"`
OIDCSecret string `yaml:"oidc.secret" env:"OG_OIDC_SECRET"`
OIDCDiscoveryUrl string `yaml:"oidc.discovery-url" env:"OG_OIDC_DISCOVERY_URL"`
}
func configWithDefaults() (*config, error) {
@ -222,5 +226,9 @@ func checks(c *config) error {
return err
}
if _, err := url.Parse(c.OIDCDiscoveryUrl); err != nil {
return err
}
return nil
}

View file

@ -15,6 +15,7 @@ type User struct {
AvatarURL string
GithubID string
GiteaID string
OIDCID string `gorm:"column:oidc_id"`
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
@ -124,6 +125,8 @@ func GetUserByProvider(id string, provider string) (*User, error) {
err = db.Where("github_id = ?", id).First(&user).Error
case "gitea":
err = db.Where("gitea_id = ?", id).First(&user).Error
case "openid-connect":
err = db.Where("oidc_id = ?", id).First(&user).Error
}
return user, err
@ -169,6 +172,11 @@ func (user *User) DeleteProviderID(provider string) error {
Update("gitea_id", nil).
Update("avatar_url", nil).
Error
case "openid-connect":
return db.Model(&user).
Update("oidc_id", nil).
Update("avatar_url", nil).
Error
}
return nil

View file

@ -16,6 +16,7 @@ import (
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/gitea"
"github.com/markbates/goth/providers/github"
"github.com/markbates/goth/providers/openidConnect"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
@ -150,6 +151,9 @@ func oauthCallback(ctx echo.Context) error {
case "gitea":
currUser.GiteaID = user.UserID
currUser.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
case "openid-connect":
currUser.OIDCID = user.UserID
currUser.AvatarURL = user.AvatarURL
}
if err = currUser.Update(); err != nil {
@ -185,6 +189,9 @@ func oauthCallback(ctx echo.Context) error {
case "gitea":
userDB.GiteaID = user.UserID
userDB.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
case "openid-connect":
userDB.OIDCID = user.UserID
userDB.AvatarURL = user.AvatarURL
}
if err = userDB.Create(); err != nil {
@ -208,6 +215,8 @@ func oauthCallback(ctx echo.Context) error {
resp, err = http.Get("https://github.com/" + user.NickName + ".keys")
case "gitea":
resp, err = http.Get(urlJoin(config.C.GiteaUrl, user.NickName+".keys"))
case "openid-connect":
err = errors.New("cannot get keys from OIDC provider")
}
if err == nil {
@ -282,6 +291,22 @@ func oauth(ctx echo.Context) error {
urlJoin(config.C.GiteaUrl, "/api/v1/user"),
),
)
case "openid-connect":
oidcProvider, err := openidConnect.New(
config.C.OIDCClientKey,
config.C.OIDCSecret,
urlJoin(opengistUrl, "/oauth/openid-connect/callback"),
config.C.OIDCDiscoveryUrl,
"openid",
"email",
"profile",
)
if err != nil {
return errorRes(500, "Cannot create OIDC provider", err)
}
goth.UseProviders(oidcProvider)
}
currUser := getUserLogged(ctx)
@ -299,6 +324,11 @@ func oauth(ctx echo.Context) error {
isDelete = true
err = currUser.DeleteProviderID(provider)
}
case "openid-connect":
if currUser.OIDCID != "" {
isDelete = true
err = currUser.DeleteProviderID(provider)
}
}
if err != nil {
@ -313,7 +343,7 @@ func oauth(ctx echo.Context) error {
ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
if provider != "github" && provider != "gitea" {
if provider != "github" && provider != "gitea" && provider != "openid-connect" {
return errorRes(400, "Unsupported provider", nil)
}

View file

@ -307,6 +307,7 @@ func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
setData(ctx, "githubOauth", config.C.GithubClientKey != "" && config.C.GithubSecret != "")
setData(ctx, "giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "")
setData(ctx, "oidcOauth", config.C.OIDCClientKey != "" && config.C.OIDCSecret != "" && config.C.OIDCDiscoveryUrl != "")
return next(ctx)
}

View file

@ -55,6 +55,9 @@
<dt>Gitea client Key</dt><dd>{{ .c.GiteaClientKey }}</dd>
<dt>Gitea Secret</dt><dd>{{ .c.GiteaSecret }}</dd>
<dt>Gitea URL</dt><dd>{{ .c.GiteaUrl }}</dd>
<dt>OIDC client Key</dt><dd>{{ .c.OIDCClientKey }}</dd>
<dt>OIDC Secret</dt><dd>{{ .c.OIDCSecret }}</dd>
<dt>OIDC Discovery URL</dt><dd>{{ .c.OIDCDiscoveryUrl }}</dd>
</dl>
</div>
<div>

View file

@ -51,7 +51,7 @@
{{ .csrfHtml }}
</form>
{{ end }}
{{ if or .githubOauth .giteaOauth }}
{{ if or .githubOauth .giteaOauth .oidcOauth }}
{{ if not .disableForm }}
<div class="relative my-4">
<div class="absolute inset-0 flex items-center" aria-hidden="true">
@ -71,6 +71,11 @@
{{ .locale.Tr "auth.gitea-oauth" }}
</a>
{{ end }}
{{ if .oidcOauth }}
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
Continue with OpenID account
</a>
{{ end }}
</div>
{{ end }}
</div>

View file

@ -7,7 +7,7 @@
</header>
<main>
<div class="space-y-4">
<div class="sm:grid {{ if or .githubOauth .giteaOauth }}grid-cols-3{{else}}grid-cols-2{{end}} gap-x-4 md:gap-x-8">
<div class="sm:grid {{ if or .githubOauth .giteaOauth .oidcOauth }}grid-cols-3{{else}}grid-cols-2{{end}} gap-x-4 md:gap-x-8">
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
@ -27,7 +27,7 @@
</form>
</div>
</div>
{{ if or .githubOauth .giteaOauth }}
{{ if or .githubOauth .giteaOauth .oidcOauth }}
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300 mb-2">
@ -60,6 +60,18 @@
</a>
{{ end }}
{{ end }}
{{ if .oidcOauth }}
{{ if .userLogged.OIDCID }}
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your OpenID account? You may lose access to Opengist if it\'s your only way to log in.')">
Unlink OpenID account
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
Link OpenID account
</a>
{{ end }}
{{ end }}
</div>
</div>