mirror of
https://github.com/m1k1o/neko.git
synced 2026-11-03 08:21:50 -06:00
feat: implement user email restrictions for OAuth login
This commit is contained in:
@@ -101,6 +101,10 @@ func oauthServiceError(err error, start bool) error {
|
|||||||
return utils.HttpForbidden("logins are locked").WithInternalErr(err)
|
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:") {
|
if start && strings.HasPrefix(err.Error(), "issuer discovery:") {
|
||||||
return utils.HttpError(http.StatusServiceUnavailable, "OAuth issuer discovery failed").WithInternalErr(err)
|
return utils.HttpError(http.StatusServiceUnavailable, "OAuth issuer discovery failed").WithInternalErr(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ func (Member) Init(cmd *cobra.Command) error {
|
|||||||
return err
|
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")
|
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 {
|
if err := viper.BindPFlag("member.oauth.client_id", cmd.PersistentFlags().Lookup("member.oauth.client_id")); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -231,6 +236,7 @@ func (s *Member) Set() {
|
|||||||
s.OAuth.AutoRedirect = viper.GetBool("member.oauth.auto_redirect")
|
s.OAuth.AutoRedirect = viper.GetBool("member.oauth.auto_redirect")
|
||||||
s.OAuth.Name = viper.GetString("member.oauth.name")
|
s.OAuth.Name = viper.GetString("member.oauth.name")
|
||||||
s.OAuth.AdminEmails = viper.GetStringSlice("member.oauth.admin_emails")
|
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.ClientID = viper.GetString("member.oauth.client_id")
|
||||||
s.OAuth.ClientSecret = viper.GetString("member.oauth.client_secret")
|
s.OAuth.ClientSecret = viper.GetString("member.oauth.client_secret")
|
||||||
s.OAuth.IssuerURL = viper.GetString("member.oauth.issuer_url")
|
s.OAuth.IssuerURL = viper.GetString("member.oauth.issuer_url")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ type Config struct {
|
|||||||
AutoRedirect bool
|
AutoRedirect bool
|
||||||
Name string
|
Name string
|
||||||
AdminEmails []string
|
AdminEmails []string
|
||||||
|
UserEmails []string
|
||||||
ClientID string
|
ClientID string
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
IssuerURL string
|
IssuerURL string
|
||||||
|
|||||||
@@ -167,9 +167,14 @@ func (service *Service) Complete(ctx context.Context, stateToken, code string) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
isAdmin := boolClaim(claims, "isAdmin") || boolClaim(idClaims, "isAdmin")
|
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
|
var profile types.MemberProfile
|
||||||
if administrator(email, isAdmin, config.AdminEmails) {
|
if admin {
|
||||||
profile = config.AdminProfile
|
profile = config.AdminProfile
|
||||||
} else {
|
} else {
|
||||||
profile = config.UserProfile
|
profile = config.UserProfile
|
||||||
|
|||||||
@@ -74,6 +74,19 @@ func administrator(email string, isAdmin bool, emails []string) bool {
|
|||||||
if isAdmin {
|
if isAdmin {
|
||||||
return true
|
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 {
|
for _, candidate := range emails {
|
||||||
if email != "" && strings.EqualFold(strings.TrimSpace(email), strings.TrimSpace(candidate)) {
|
if email != "" && strings.EqualFold(strings.TrimSpace(email), strings.TrimSpace(candidate)) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ var (
|
|||||||
ErrSessionAlreadyConnected = errors.New("session is already connected")
|
ErrSessionAlreadyConnected = errors.New("session is already connected")
|
||||||
ErrSessionLoginDisabled = errors.New("session login disabled")
|
ErrSessionLoginDisabled = errors.New("session login disabled")
|
||||||
ErrSessionLoginsLocked = errors.New("session logins locked")
|
ErrSessionLoginsLocked = errors.New("session logins locked")
|
||||||
|
ErrSessionEmailNotAllowed = errors.New("email address is not allowed to log in")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cursor struct {
|
type Cursor struct {
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ member:
|
|||||||
auto_redirect: true
|
auto_redirect: true
|
||||||
name: "Example SSO"
|
name: "Example SSO"
|
||||||
admin_emails: ["platform-admin@example.com", "security@example.com"]
|
admin_emails: ["platform-admin@example.com", "security@example.com"]
|
||||||
|
user_emails: [] # if empty, any authenticated user is allowed to log in
|
||||||
client_id: "<client-id>"
|
client_id: "<client-id>"
|
||||||
client_secret: "<client-secret>"
|
client_secret: "<client-secret>"
|
||||||
issuer_url: "https://id.example.com"
|
issuer_url: "https://id.example.com"
|
||||||
@@ -227,6 +228,8 @@ The sign-in endpoint is `GET /api/oauth/login`; the callback endpoint is `GET /a
|
|||||||
|
|
||||||
`admin_emails` is a list matched against the standard `email` field, case-insensitively. An OAuth user is an administrator when either their email matches that list or their user-info/ID-token claims include `isAdmin: true`; their permissions then use `admin_profile`. The configured name and avatar fields are read from userinfo first, then fall back to the ID token when userinfo omits them. Avatar URLs are rendered by clients that support member avatars.
|
`admin_emails` is a list matched against the standard `email` field, case-insensitively. An OAuth user is an administrator when either their email matches that list or their user-info/ID-token claims include `isAdmin: true`; their permissions then use `admin_profile`. The configured name and avatar fields are read from userinfo first, then fall back to the ID token when userinfo omits them. Avatar URLs are rendered by clients that support member avatars.
|
||||||
|
|
||||||
|
`user_emails` restricts non-admin login to a list of allowed emails, matched the same way as `admin_emails`. If `user_emails` is empty (the default), any authenticated user is allowed to log in. Administrators (matched by `admin_emails` or `isAdmin: true`) can always log in regardless of `user_emails`. Users whose email is not allowed receive an HTTP 403 response and no session is created.
|
||||||
|
|
||||||
For OAuth sessions, `GET /api/whoami` includes `extra_data`: the merged userinfo and ID-token claims, with userinfo values taking precedence. It is returned only to the authenticated session owner and is intended for profile-field troubleshooting. Do not place secrets in provider profile claims.
|
For OAuth sessions, `GET /api/whoami` includes `extra_data`: the merged userinfo and ID-token claims, with userinfo values taking precedence. It is returned only to the authenticated session owner and is intended for profile-field troubleshooting. Do not place secrets in provider profile claims.
|
||||||
|
|
||||||
For non-OIDC providers, set `authorization_url`, `token_url`, `userinfo_url`, and optionally `redirect_url` directly. For GitHub, for example, use `read:user` as a scope and set `subject_field: id`, `username_field: login`, and `avatar_field: avatar_url`.
|
For non-OIDC providers, set `authorization_url`, `token_url`, `userinfo_url`, and optionally `redirect_url` directly. For GitHub, for example, use `read:user` as a scope and set `subject_field: id`, `username_field: login`, and `avatar_field: avatar_url`.
|
||||||
|
|||||||
Reference in New Issue
Block a user