mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-08-19 17:01:12 +00:00
Add a new config option for OAuth2 authentication sources: allow users to change their username. In the case where OAuth2 is more like a social OAuth2 login there's no need to not allow users to change their username. The information how the user is linked to the authentication source is stored in different fields. Resolves forgejo/forgejo#687 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8714 Reviewed-by: 0ko <0ko@noreply.codeberg.org> Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: Gusted <postmaster@gusted.xyz> Co-committed-by: Gusted <postmaster@gusted.xyz>
402 lines
13 KiB
Go
402 lines
13 KiB
Go
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package admin
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"forgejo.org/models/auth"
|
|
"forgejo.org/models/db"
|
|
"forgejo.org/modules/auth/pam"
|
|
"forgejo.org/modules/base"
|
|
"forgejo.org/modules/log"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/web"
|
|
auth_service "forgejo.org/services/auth"
|
|
"forgejo.org/services/auth/source/ldap"
|
|
"forgejo.org/services/auth/source/oauth2"
|
|
pam_service "forgejo.org/services/auth/source/pam"
|
|
"forgejo.org/services/auth/source/smtp"
|
|
"forgejo.org/services/context"
|
|
"forgejo.org/services/forms"
|
|
|
|
"xorm.io/xorm/convert"
|
|
)
|
|
|
|
const (
|
|
tplAuths base.TplName = "admin/auth/list"
|
|
tplAuthNew base.TplName = "admin/auth/new"
|
|
tplAuthEdit base.TplName = "admin/auth/edit"
|
|
)
|
|
|
|
// Authentications show authentication config page
|
|
func Authentications(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("admin.authentication")
|
|
ctx.Data["PageIsAdminAuthentications"] = true
|
|
|
|
var err error
|
|
ctx.Data["Sources"], ctx.Data["Total"], err = db.FindAndCount[auth.Source](ctx, auth.FindSourcesOptions{})
|
|
if err != nil {
|
|
ctx.ServerError("auth.Sources", err)
|
|
return
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, tplAuths)
|
|
}
|
|
|
|
type dropdownItem struct {
|
|
Name string
|
|
Type any
|
|
}
|
|
|
|
var (
|
|
authSources = func() []dropdownItem {
|
|
items := []dropdownItem{
|
|
{auth.LDAP.String(), auth.LDAP},
|
|
{auth.DLDAP.String(), auth.DLDAP},
|
|
{auth.SMTP.String(), auth.SMTP},
|
|
{auth.OAuth2.String(), auth.OAuth2},
|
|
}
|
|
if pam.Supported {
|
|
items = append(items, dropdownItem{auth.Names[auth.PAM], auth.PAM})
|
|
}
|
|
return items
|
|
}()
|
|
|
|
securityProtocols = []dropdownItem{
|
|
{ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
|
|
{ldap.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
|
|
{ldap.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
|
|
}
|
|
)
|
|
|
|
// NewAuthSource render adding a new auth source page
|
|
func NewAuthSource(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("admin.auths.new")
|
|
ctx.Data["PageIsAdminAuthentications"] = true
|
|
|
|
ctx.Data["type"] = auth.LDAP.Int()
|
|
ctx.Data["CurrentTypeName"] = auth.Names[auth.LDAP]
|
|
ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
|
|
ctx.Data["smtp_auth"] = "PLAIN"
|
|
ctx.Data["is_active"] = true
|
|
ctx.Data["is_sync_enabled"] = true
|
|
ctx.Data["AuthSources"] = authSources
|
|
ctx.Data["SecurityProtocols"] = securityProtocols
|
|
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
|
oauth2providers := oauth2.GetSupportedOAuth2Providers()
|
|
ctx.Data["OAuth2Providers"] = oauth2providers
|
|
|
|
// only the first as default
|
|
ctx.Data["oauth2_provider"] = oauth2providers[0].Name()
|
|
|
|
ctx.HTML(http.StatusOK, tplAuthNew)
|
|
}
|
|
|
|
func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
|
|
var pageSize uint32
|
|
if form.UsePagedSearch {
|
|
pageSize = uint32(form.SearchPageSize)
|
|
}
|
|
return &ldap.Source{
|
|
Name: form.Name,
|
|
Host: form.Host,
|
|
Port: form.Port,
|
|
SecurityProtocol: ldap.SecurityProtocol(form.SecurityProtocol),
|
|
SkipVerify: form.SkipVerify,
|
|
BindDN: form.BindDN,
|
|
UserDN: form.UserDN,
|
|
BindPassword: form.BindPassword,
|
|
UserBase: form.UserBase,
|
|
DefaultDomainName: form.DefaultDomainName,
|
|
AttributeUsername: form.AttributeUsername,
|
|
AttributeName: form.AttributeName,
|
|
AttributeSurname: form.AttributeSurname,
|
|
AttributeMail: form.AttributeMail,
|
|
AttributesInBind: form.AttributesInBind,
|
|
AttributeSSHPublicKey: form.AttributeSSHPublicKey,
|
|
AttributeAvatar: form.AttributeAvatar,
|
|
SearchPageSize: pageSize,
|
|
Filter: form.Filter,
|
|
GroupsEnabled: form.GroupsEnabled,
|
|
GroupDN: form.GroupDN,
|
|
GroupFilter: form.GroupFilter,
|
|
GroupMemberUID: form.GroupMemberUID,
|
|
GroupTeamMap: form.GroupTeamMap,
|
|
GroupTeamMapRemoval: form.GroupTeamMapRemoval,
|
|
UserUID: form.UserUID,
|
|
AdminFilter: form.AdminFilter,
|
|
RestrictedFilter: form.RestrictedFilter,
|
|
AllowDeactivateAll: form.AllowDeactivateAll,
|
|
Enabled: true,
|
|
SkipLocalTwoFA: form.SkipLocalTwoFA,
|
|
}
|
|
}
|
|
|
|
func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
|
|
return &smtp.Source{
|
|
Auth: form.SMTPAuth,
|
|
Host: form.SMTPHost,
|
|
Port: form.SMTPPort,
|
|
AllowedDomains: form.AllowedDomains,
|
|
ForceSMTPS: form.ForceSMTPS,
|
|
SkipVerify: form.SkipVerify,
|
|
HeloHostname: form.HeloHostname,
|
|
DisableHelo: form.DisableHelo,
|
|
SkipLocalTwoFA: form.SkipLocalTwoFA,
|
|
}
|
|
}
|
|
|
|
func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
|
|
var customURLMapping *oauth2.CustomURLMapping
|
|
if form.Oauth2UseCustomURL {
|
|
customURLMapping = &oauth2.CustomURLMapping{
|
|
TokenURL: form.Oauth2TokenURL,
|
|
AuthURL: form.Oauth2AuthURL,
|
|
ProfileURL: form.Oauth2ProfileURL,
|
|
EmailURL: form.Oauth2EmailURL,
|
|
Tenant: form.Oauth2Tenant,
|
|
}
|
|
} else {
|
|
customURLMapping = nil
|
|
}
|
|
var scopes []string
|
|
for _, s := range strings.Split(form.Oauth2Scopes, ",") {
|
|
s = strings.TrimSpace(s)
|
|
if s != "" {
|
|
scopes = append(scopes, s)
|
|
}
|
|
}
|
|
|
|
return &oauth2.Source{
|
|
Provider: form.Oauth2Provider,
|
|
ClientID: form.Oauth2Key,
|
|
ClientSecret: form.Oauth2Secret,
|
|
OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL,
|
|
CustomURLMapping: customURLMapping,
|
|
IconURL: form.Oauth2IconURL,
|
|
Scopes: scopes,
|
|
AttributeSSHPublicKey: form.Oauth2AttributeSSHPublicKey,
|
|
RequiredClaimName: form.Oauth2RequiredClaimName,
|
|
RequiredClaimValue: form.Oauth2RequiredClaimValue,
|
|
SkipLocalTwoFA: form.SkipLocalTwoFA,
|
|
GroupClaimName: form.Oauth2GroupClaimName,
|
|
RestrictedGroup: form.Oauth2RestrictedGroup,
|
|
AdminGroup: form.Oauth2AdminGroup,
|
|
GroupTeamMap: form.Oauth2GroupTeamMap,
|
|
GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval,
|
|
AllowUsernameChange: form.AllowUsernameChange,
|
|
}
|
|
}
|
|
|
|
// NewAuthSourcePost response for adding an auth source
|
|
func NewAuthSourcePost(ctx *context.Context) {
|
|
form := *web.GetForm(ctx).(*forms.AuthenticationForm)
|
|
ctx.Data["Title"] = ctx.Tr("admin.auths.new")
|
|
ctx.Data["PageIsAdminAuthentications"] = true
|
|
|
|
ctx.Data["CurrentTypeName"] = auth.Type(form.Type).String()
|
|
ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
|
|
ctx.Data["AuthSources"] = authSources
|
|
ctx.Data["SecurityProtocols"] = securityProtocols
|
|
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
|
oauth2providers := oauth2.GetSupportedOAuth2Providers()
|
|
ctx.Data["OAuth2Providers"] = oauth2providers
|
|
|
|
hasTLS := false
|
|
var config convert.Conversion
|
|
switch auth.Type(form.Type) {
|
|
case auth.LDAP, auth.DLDAP:
|
|
config = parseLDAPConfig(form)
|
|
hasTLS = ldap.SecurityProtocol(form.SecurityProtocol) > ldap.SecurityProtocolUnencrypted
|
|
case auth.SMTP:
|
|
config = parseSMTPConfig(form)
|
|
hasTLS = true
|
|
case auth.PAM:
|
|
config = &pam_service.Source{
|
|
ServiceName: form.PAMServiceName,
|
|
EmailDomain: form.PAMEmailDomain,
|
|
SkipLocalTwoFA: form.SkipLocalTwoFA,
|
|
}
|
|
case auth.OAuth2:
|
|
config = parseOAuth2Config(form)
|
|
oauth2Config := config.(*oauth2.Source)
|
|
if oauth2Config.Provider == "openidConnect" {
|
|
discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
|
|
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
|
|
ctx.Data["Err_DiscoveryURL"] = true
|
|
ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form)
|
|
return
|
|
}
|
|
}
|
|
default:
|
|
ctx.Error(http.StatusBadRequest)
|
|
return
|
|
}
|
|
ctx.Data["HasTLS"] = hasTLS
|
|
|
|
if ctx.HasError() {
|
|
ctx.HTML(http.StatusOK, tplAuthNew)
|
|
return
|
|
}
|
|
|
|
if err := auth.CreateSource(ctx, &auth.Source{
|
|
Type: auth.Type(form.Type),
|
|
Name: form.Name,
|
|
IsActive: form.IsActive,
|
|
IsSyncEnabled: form.IsSyncEnabled,
|
|
Cfg: config,
|
|
}); err != nil {
|
|
if auth.IsErrSourceAlreadyExist(err) {
|
|
ctx.Data["Err_Name"] = true
|
|
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form)
|
|
} else if oauth2.IsErrOpenIDConnectInitialize(err) {
|
|
ctx.Data["Err_DiscoveryURL"] = true
|
|
unwrapped := err.(oauth2.ErrOpenIDConnectInitialize).Unwrap()
|
|
ctx.RenderWithErr(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form)
|
|
} else {
|
|
ctx.ServerError("auth.CreateSource", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
log.Trace("Authentication created by admin(%s): %s", ctx.Doer.Name, form.Name)
|
|
|
|
ctx.Flash.Success(ctx.Tr("admin.auths.new_success", form.Name))
|
|
ctx.Redirect(setting.AppSubURL + "/admin/auths")
|
|
}
|
|
|
|
// EditAuthSource render editing auth source page
|
|
func EditAuthSource(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
|
|
ctx.Data["PageIsAdminAuthentications"] = true
|
|
|
|
ctx.Data["SecurityProtocols"] = securityProtocols
|
|
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
|
oauth2providers := oauth2.GetSupportedOAuth2Providers()
|
|
ctx.Data["OAuth2Providers"] = oauth2providers
|
|
|
|
source, err := auth.GetSourceByID(ctx, ctx.ParamsInt64(":authid"))
|
|
if err != nil {
|
|
ctx.ServerError("auth.GetSourceByID", err)
|
|
return
|
|
}
|
|
ctx.Data["Source"] = source
|
|
ctx.Data["HasTLS"] = source.HasTLS()
|
|
|
|
if source.IsOAuth2() {
|
|
type Named interface {
|
|
Name() string
|
|
}
|
|
|
|
for _, provider := range oauth2providers {
|
|
if provider.Name() == source.Cfg.(Named).Name() {
|
|
ctx.Data["CurrentOAuth2Provider"] = provider
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, tplAuthEdit)
|
|
}
|
|
|
|
// EditAuthSourcePost response for editing auth source
|
|
func EditAuthSourcePost(ctx *context.Context) {
|
|
form := *web.GetForm(ctx).(*forms.AuthenticationForm)
|
|
ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
|
|
ctx.Data["PageIsAdminAuthentications"] = true
|
|
|
|
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
|
oauth2providers := oauth2.GetSupportedOAuth2Providers()
|
|
ctx.Data["OAuth2Providers"] = oauth2providers
|
|
|
|
source, err := auth.GetSourceByID(ctx, ctx.ParamsInt64(":authid"))
|
|
if err != nil {
|
|
ctx.ServerError("auth.GetSourceByID", err)
|
|
return
|
|
}
|
|
ctx.Data["Source"] = source
|
|
ctx.Data["HasTLS"] = source.HasTLS()
|
|
|
|
if ctx.HasError() {
|
|
ctx.HTML(http.StatusOK, tplAuthEdit)
|
|
return
|
|
}
|
|
|
|
var config convert.Conversion
|
|
switch auth.Type(form.Type) {
|
|
case auth.LDAP, auth.DLDAP:
|
|
config = parseLDAPConfig(form)
|
|
case auth.SMTP:
|
|
config = parseSMTPConfig(form)
|
|
case auth.PAM:
|
|
config = &pam_service.Source{
|
|
ServiceName: form.PAMServiceName,
|
|
EmailDomain: form.PAMEmailDomain,
|
|
}
|
|
case auth.OAuth2:
|
|
config = parseOAuth2Config(form)
|
|
oauth2Config := config.(*oauth2.Source)
|
|
if oauth2Config.Provider == "openidConnect" {
|
|
discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
|
|
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
|
|
ctx.Data["Err_DiscoveryURL"] = true
|
|
ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form)
|
|
return
|
|
}
|
|
}
|
|
default:
|
|
ctx.Error(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
source.Name = form.Name
|
|
source.IsActive = form.IsActive
|
|
source.IsSyncEnabled = form.IsSyncEnabled
|
|
source.Cfg = config
|
|
if err := auth.UpdateSource(ctx, source); err != nil {
|
|
if auth.IsErrSourceAlreadyExist(err) {
|
|
ctx.Data["Err_Name"] = true
|
|
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthEdit, form)
|
|
} else if oauth2.IsErrOpenIDConnectInitialize(err) {
|
|
ctx.Flash.Error(err.Error(), true)
|
|
ctx.Data["Err_DiscoveryURL"] = true
|
|
ctx.HTML(http.StatusOK, tplAuthEdit)
|
|
} else {
|
|
ctx.ServerError("UpdateSource", err)
|
|
}
|
|
return
|
|
}
|
|
log.Trace("Authentication changed by admin(%s): %d", ctx.Doer.Name, source.ID)
|
|
|
|
ctx.Flash.Success(ctx.Tr("admin.auths.update_success"))
|
|
ctx.Redirect(setting.AppSubURL + "/admin/auths/" + strconv.FormatInt(form.ID, 10))
|
|
}
|
|
|
|
// DeleteAuthSource response for deleting an auth source
|
|
func DeleteAuthSource(ctx *context.Context) {
|
|
source, err := auth.GetSourceByID(ctx, ctx.ParamsInt64(":authid"))
|
|
if err != nil {
|
|
ctx.ServerError("auth.GetSourceByID", err)
|
|
return
|
|
}
|
|
|
|
if err = auth_service.DeleteSource(ctx, source); err != nil {
|
|
if auth.IsErrSourceInUse(err) {
|
|
ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used"))
|
|
} else {
|
|
ctx.Flash.Error(fmt.Sprintf("auth_service.DeleteSource: %v", err))
|
|
}
|
|
ctx.JSONRedirect(setting.AppSubURL + "/admin/auths/" + url.PathEscape(ctx.Params(":authid")))
|
|
return
|
|
}
|
|
log.Trace("Authentication deleted by admin(%s): %d", ctx.Doer.Name, source.ID)
|
|
|
|
ctx.Flash.Success(ctx.Tr("admin.auths.deletion_success"))
|
|
ctx.JSONRedirect(setting.AppSubURL + "/admin/auths")
|
|
}
|