feat: implement user email restrictions for OAuth login

This commit is contained in:
Miroslav Šedivý
2026-09-23 23:52:47 +02:00
parent d991fec3c7
commit 3f3ae10fb9
7 changed files with 34 additions and 1 deletions
+4
View File
@@ -101,6 +101,10 @@ func oauthServiceError(err error, start bool) error {
return utils.HttpForbidden("logins are locked").WithInternalErr(err)
}
if errors.Is(err, types.ErrSessionEmailNotAllowed) {
return utils.HttpForbidden("email address is not allowed to log in").WithInternalErr(err)
}
if start && strings.HasPrefix(err.Error(), "issuer discovery:") {
return utils.HttpError(http.StatusServiceUnavailable, "OAuth issuer discovery failed").WithInternalErr(err)
}
+6
View File
@@ -112,6 +112,11 @@ func (Member) Init(cmd *cobra.Command) error {
return err
}
cmd.PersistentFlags().StringSlice("member.oauth.user_emails", []string{}, "OAuth user-info email addresses allowed to log in, if empty all are allowed")
if err := viper.BindPFlag("member.oauth.user_emails", cmd.PersistentFlags().Lookup("member.oauth.user_emails")); err != nil {
return err
}
cmd.PersistentFlags().String("member.oauth.client_id", "", "OAuth 2.0 client ID")
if err := viper.BindPFlag("member.oauth.client_id", cmd.PersistentFlags().Lookup("member.oauth.client_id")); err != nil {
return err
@@ -231,6 +236,7 @@ func (s *Member) Set() {
s.OAuth.AutoRedirect = viper.GetBool("member.oauth.auto_redirect")
s.OAuth.Name = viper.GetString("member.oauth.name")
s.OAuth.AdminEmails = viper.GetStringSlice("member.oauth.admin_emails")
s.OAuth.UserEmails = viper.GetStringSlice("member.oauth.user_emails")
s.OAuth.ClientID = viper.GetString("member.oauth.client_id")
s.OAuth.ClientSecret = viper.GetString("member.oauth.client_secret")
s.OAuth.IssuerURL = viper.GetString("member.oauth.issuer_url")
+1
View File
@@ -9,6 +9,7 @@ type Config struct {
AutoRedirect bool
Name string
AdminEmails []string
UserEmails []string
ClientID string
ClientSecret string
IssuerURL string
+6 -1
View File
@@ -167,9 +167,14 @@ func (service *Service) Complete(ctx context.Context, stateToken, code string) (
}
isAdmin := boolClaim(claims, "isAdmin") || boolClaim(idClaims, "isAdmin")
admin := administrator(email, isAdmin, config.AdminEmails)
if !admin && !emailAllowed(email, config.UserEmails) {
return nil, "", types.ErrSessionEmailNotAllowed
}
var profile types.MemberProfile
if administrator(email, isAdmin, config.AdminEmails) {
if admin {
profile = config.AdminProfile
} else {
profile = config.UserProfile
+13
View File
@@ -74,6 +74,19 @@ func administrator(email string, isAdmin bool, emails []string) bool {
if isAdmin {
return true
}
return emailMatches(email, emails)
}
// emailAllowed reports whether email is permitted to log in given a user
// allowlist. An empty allowlist permits every email.
func emailAllowed(email string, emails []string) bool {
if len(emails) == 0 {
return true
}
return emailMatches(email, emails)
}
func emailMatches(email string, emails []string) bool {
for _, candidate := range emails {
if email != "" && strings.EqualFold(strings.TrimSpace(email), strings.TrimSpace(candidate)) {
return true
+1
View File
@@ -12,6 +12,7 @@ var (
ErrSessionAlreadyConnected = errors.New("session is already connected")
ErrSessionLoginDisabled = errors.New("session login disabled")
ErrSessionLoginsLocked = errors.New("session logins locked")
ErrSessionEmailNotAllowed = errors.New("email address is not allowed to log in")
)
type Cursor struct {