Add GitLab OAuth provider (#174)

This commit is contained in:
Thomas Miceli 2023-12-18 01:35:44 +01:00
parent c9fd58c904
commit 8466e50cc3
10 changed files with 150 additions and 94 deletions

View file

@ -55,12 +55,18 @@ ssh.keygen-executable: ssh-keygen
# OAuth2 configuration
# The callback/redirect URL must be http://opengist.domain/oauth/<github|gitea|openid-connect>/callback
# The callback/redirect URL must be http://opengist.domain/oauth/<github|gitlab|gitea|openid-connect>/callback
# To create a new OAuth2 application using GitHub : https://github.com/settings/applications/new
github.client-key:
github.secret:
# To create a new OAuth2 application using Gitlab : https://gitlab.com/-/user_settings/applications
gitlab.client-key:
gitlab.secret:
# URL of the Gitlab instance. Default: https://gitlab.com/
gitlab.url: https://gitlab.com/
# To create a new OAuth2 application using Gitea : https://gitea.domain/user/settings/applications
gitea.client-key:
gitea.secret:

View file

@ -13,6 +13,19 @@ Opengist can be configured to use OAuth to authenticate users, with GitHub, Gite
```
## GitLab
* Add a new OAuth app in Application settings from the [GitLab instance](https://gitlab.com/-/user_settings/applications)
* Set 'Redirect URI' to `http://opengist.domain/oauth/gitlab/callback`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
```yaml
gitlab.client-key: <key>
gitlab.secret: <secret>
# URL of the Gitlab instance. Default: https://gitlab.com/
gitlab.url: https://gitlab.com/
```
## Gitea
* Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications)

View file

@ -1,25 +1,28 @@
# Configuration Cheat Sheet
| 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. |
| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. |
| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) |
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. |
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. |
| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. |
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
| 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. |
| 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. |
| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. |
| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) |
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. |
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. |
| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. |
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. |
| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. |
| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. |
| 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. |

View file

@ -42,6 +42,10 @@ type config struct {
GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"`
GithubSecret string `yaml:"github.secret" env:"OG_GITHUB_SECRET"`
GitlabClientKey string `yaml:"gitlab.client-key" env:"OG_GITLAB_CLIENT_KEY"`
GitlabSecret string `yaml:"gitlab.secret" env:"OG_GITLAB_SECRET"`
GitlabUrl string `yaml:"gitlab.url" env:"OG_GITLAB_URL"`
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"`
@ -69,7 +73,7 @@ func configWithDefaults() (*config, error) {
c.SshPort = "2222"
c.SshKeygen = "ssh-keygen"
c.GiteaUrl = "http://gitea.com"
c.GiteaUrl = "https://gitea.com"
return c, nil
}

View file

@ -14,6 +14,7 @@ type User struct {
MD5Hash string // for gravatar, if no Email is specified, the value is random
AvatarURL string
GithubID string
GitlabID string
GiteaID string
OIDCID string `gorm:"column:oidc_id"`
@ -128,6 +129,8 @@ func GetUserByProvider(id string, provider string) (*User, error) {
switch provider {
case "github":
err = db.Where("github_id = ?", id).First(&user).Error
case "gitlab":
err = db.Where("gitlab_id = ?", id).First(&user).Error
case "gitea":
err = db.Where("gitea_id = ?", id).First(&user).Error
case "openid-connect":
@ -166,20 +169,16 @@ func (user *User) HasLiked(gist *Gist) (bool, error) {
}
func (user *User) DeleteProviderID(provider string) error {
switch provider {
case "github":
providerIDFields := map[string]string{
"github": "github_id",
"gitlab": "gitlab_id",
"gitea": "gitea_id",
"openid-connect": "oidc_id",
}
if providerIDField, ok := providerIDFields[provider]; ok {
return db.Model(&user).
Update("github_id", nil).
Update("avatar_url", nil).
Error
case "gitea":
return db.Model(&user).
Update("gitea_id", nil).
Update("avatar_url", nil).
Error
case "openid-connect":
return db.Model(&user).
Update("oidc_id", nil).
Update(providerIDField, nil).
Update("avatar_url", nil).
Error
}

View file

@ -92,8 +92,10 @@ settings.email-help: Used for commits and Gravatar
settings.email-set: Set email
settings.link-accounts: Link accounts
settings.link-github-account: Link GitHub account
settings.link-gitlab-account: Link Gitlab account
settings.link-gitea-account: Link Gitea account
settings.unlink-github-account: Unlink GitHub account
settings.unlink-gitlab-account: Unlink Gitlab account
settings.unlink-gitea-account: Unlink Gitea account
settings.delete-account: Delete account
settings.delete-account-confirm: Are you sure you want to delete your account ?
@ -121,6 +123,7 @@ auth.password: Password
auth.register-instead: Register instead
auth.login-instead: Login instead
auth.github-oauth: Continue with GitHub account
auth.gitlab-oauth: Continue with Gitlab account
auth.gitea-oauth: Continue with Gitea account
error: Error

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/gitlab"
"github.com/markbates/goth/providers/openidConnect"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
@ -25,6 +26,13 @@ import (
"gorm.io/gorm"
)
const (
GitHubProvider = "github"
GitLabProvider = "gitlab"
GiteaProvider = "gitea"
OpenIDConnect = "openid-connect"
)
var title = cases.Title(language.English)
func register(ctx echo.Context) error {
@ -146,17 +154,7 @@ func oauthCallback(ctx echo.Context) error {
currUser := getUserLogged(ctx)
if currUser != nil {
// if user is logged in, link account to user and update its avatar URL
switch user.Provider {
case "github":
currUser.GithubID = user.UserID
currUser.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
case "gitea":
currUser.GiteaID = user.UserID
currUser.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
case "openid-connect":
currUser.OIDCID = user.UserID
currUser.AvatarURL = user.AvatarURL
}
updateUserProviderInfo(currUser, user.Provider, user)
if err = currUser.Update(); err != nil {
return errorRes(500, "Cannot update user "+title.String(user.Provider)+" id", err)
@ -184,17 +182,7 @@ func oauthCallback(ctx echo.Context) error {
}
// set provider id and avatar URL
switch user.Provider {
case "github":
userDB.GithubID = user.UserID
userDB.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
case "gitea":
userDB.GiteaID = user.UserID
userDB.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
case "openid-connect":
userDB.OIDCID = user.UserID
userDB.AvatarURL = user.AvatarURL
}
updateUserProviderInfo(userDB, user.Provider, user)
if err = userDB.Create(); err != nil {
if db.IsUniqueConstraintViolation(err) {
@ -213,11 +201,13 @@ func oauthCallback(ctx echo.Context) error {
var resp *http.Response
switch user.Provider {
case "github":
case GitHubProvider:
resp, err = http.Get("https://github.com/" + user.NickName + ".keys")
case "gitea":
case GitLabProvider:
resp, err = http.Get(urlJoin(config.C.GitlabUrl, user.NickName+".keys"))
case GiteaProvider:
resp, err = http.Get(urlJoin(config.C.GiteaUrl, user.NickName+".keys"))
case "openid-connect":
case OpenIDConnect:
err = errors.New("cannot get keys from OIDC provider")
}
@ -273,7 +263,7 @@ func oauth(ctx echo.Context) error {
}
switch provider {
case "github":
case GitHubProvider:
goth.UseProviders(
github.New(
config.C.GithubClientKey,
@ -282,7 +272,19 @@ func oauth(ctx echo.Context) error {
),
)
case "gitea":
case GitLabProvider:
goth.UseProviders(
gitlab.NewCustomisedURL(
config.C.GitlabClientKey,
config.C.GitlabSecret,
urlJoin(opengistUrl, "/oauth/gitlab/callback"),
urlJoin(config.C.GitlabUrl, "/oauth/authorize"),
urlJoin(config.C.GitlabUrl, "/oauth/token"),
urlJoin(config.C.GitlabUrl, "/api/v4/user"),
),
)
case GiteaProvider:
goth.UseProviders(
gitea.NewCustomisedURL(
config.C.GiteaClientKey,
@ -293,7 +295,7 @@ func oauth(ctx echo.Context) error {
urlJoin(config.C.GiteaUrl, "/api/v1/user"),
),
)
case "openid-connect":
case OpenIDConnect:
oidcProvider, err := openidConnect.New(
config.C.OIDCClientKey,
config.C.OIDCSecret,
@ -313,31 +315,21 @@ func oauth(ctx echo.Context) error {
currUser := getUserLogged(ctx)
if currUser != nil {
isDelete := false
var err error
switch provider {
case "github":
if currUser.GithubID != "" {
isDelete = true
err = currUser.DeleteProviderID(provider)
}
case "gitea":
if currUser.GiteaID != "" {
isDelete = true
err = currUser.DeleteProviderID(provider)
}
case "openid-connect":
if currUser.OIDCID != "" {
isDelete = true
err = currUser.DeleteProviderID(provider)
}
// Map each provider to a function that checks the relevant ID in currUser
providerIDCheckMap := map[string]func() bool{
GitHubProvider: func() bool { return currUser.GithubID != "" },
GitLabProvider: func() bool { return currUser.GitlabID != "" },
GiteaProvider: func() bool { return currUser.GiteaID != "" },
OpenIDConnect: func() bool { return currUser.OIDCID != "" },
}
if err != nil {
return errorRes(500, "Cannot unlink account from "+title.String(provider), err)
}
// Check if the provider is valid and if the user has a linked ID
// Means that the user wants to unlink the account
if checkFunc, exists := providerIDCheckMap[provider]; exists && checkFunc() {
if err := currUser.DeleteProviderID(provider); err != nil {
return errorRes(500, "Cannot unlink account from "+title.String(provider), err)
}
if isDelete {
addFlash(ctx, "Account unlinked from "+title.String(provider), "success")
return redirect(ctx, "/settings")
}
@ -345,7 +337,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" && provider != "openid-connect" {
if provider != GitHubProvider && provider != GitLabProvider && provider != GiteaProvider && provider != OpenIDConnect {
return errorRes(400, "Unsupported provider", nil)
}
@ -368,11 +360,28 @@ func urlJoin(base string, elem ...string) string {
return joined
}
func updateUserProviderInfo(userDB *db.User, provider string, user goth.User) {
userDB.AvatarURL = getAvatarUrlFromProvider(provider, user.UserID)
switch provider {
case GitHubProvider:
userDB.GithubID = user.UserID
case GitLabProvider:
userDB.GitlabID = user.UserID
case GiteaProvider:
userDB.GiteaID = user.UserID
case OpenIDConnect:
userDB.OIDCID = user.UserID
userDB.AvatarURL = user.AvatarURL
}
}
func getAvatarUrlFromProvider(provider string, identifier string) string {
switch provider {
case "github":
case GitHubProvider:
return "https://avatars.githubusercontent.com/u/" + identifier + "?v=4"
case "gitea":
case GitLabProvider:
return urlJoin(config.C.GitlabUrl, "/uploads/-/system/user/avatar/", identifier, "/avatar.png") + "?width=400"
case GiteaProvider:
resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", identifier))
if err != nil {
log.Error().Err(err).Msg("Cannot get user from Gitea")

View file

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

View file

@ -51,7 +51,7 @@
{{ .csrfHtml }}
</form>
{{ end }}
{{ if or .githubOauth .giteaOauth .oidcOauth }}
{{ if or .githubOauth .gitlabOauth .giteaOauth .oidcOauth }}
{{ if not .disableForm }}
<div class="relative my-4">
<div class="absolute inset-0 flex items-center" aria-hidden="true">
@ -66,6 +66,11 @@
{{ .locale.Tr "auth.github-oauth" }}
</a>
{{ end }}
{{ if .gitlabOauth }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitlab" 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">
{{ .locale.Tr "auth.gitlab-oauth" }}
</a>
{{ end }}
{{ if .giteaOauth }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitea" 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">
{{ .locale.Tr "auth.gitea-oauth" }}

View file

@ -7,7 +7,7 @@
</header>
<main>
<div class="space-y-4">
<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="sm:grid {{ if or .githubOauth .gitlabOauth .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 .oidcOauth }}
{{ if or .githubOauth .gitlabOauth .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">
@ -48,6 +48,19 @@
{{ end }}
{{ end }}
{{ if .gitlabOauth }}
{{ if .userLogged.GitlabID }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitlab" 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"
onclick="return confirm('Are you sure you want to unlink your Gitlab account? You may lose access to Opengist if it\'s your only way to log in.')">
{{ .locale.Tr "settings.unlink-gitlab-account" }}
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitlab" 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">
{{ .locale.Tr "settings.link-gitlab-account" }}
</a>
{{ end }}
{{ end }}
{{ if .giteaOauth }}
{{ if .userLogged.GiteaID }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitea" 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"