Skip to content

Commit 015c03d

Browse files
JimKnoxxclaude
andcommitted
Added the optional oauth allowed roles environment variable
- Setting this prevents users without the specified roles from accessing the Fider instance Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 98bf7d9 commit 015c03d

File tree

38 files changed

+596
-89
lines changed

38 files changed

+596
-89
lines changed

.example.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ OAUTH_GOOGLE_SECRET=
2323
OAUTH_GITHUB_CLIENTID=
2424
OAUTH_GITHUB_SECRET=
2525

26+
OAUTH_ALLOWED_ROLES=
27+
2628
EMAIL_NOREPLY=noreply@yourdomain.com
2729

2830
#EMAIL_MAILGUN_API=

app/actions/oauth.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type CreateEditOAuthConfig struct {
3333
JSONUserIDPath string `json:"jsonUserIDPath"`
3434
JSONUserNamePath string `json:"jsonUserNamePath"`
3535
JSONUserEmailPath string `json:"jsonUserEmailPath"`
36+
JSONUserRolesPath string `json:"jsonUserRolesPath"`
3637
}
3738

3839
func NewCreateEditOAuthConfig() *CreateEditOAuthConfig {
@@ -184,5 +185,9 @@ func (action *CreateEditOAuthConfig) Validate(ctx context.Context, user *entity.
184185
result.AddFieldFailure("jsonUserEmailPath", "JSON User Email Path must have less than 100 characters.")
185186
}
186187

188+
if len(action.JSONUserRolesPath) > 100 {
189+
result.AddFieldFailure("jsonUserRolesPath", "JSON User Roles Path must have less than 100 characters.")
190+
}
191+
187192
return result
188193
}

app/cmd/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ func routes(r *web.Engine) *web.Engine {
120120
r.Get("/signin/complete", handlers.CompleteSignInProfilePage())
121121
r.Get("/loginemailsent", handlers.LoginEmailSentPage())
122122
r.Get("/not-invited", handlers.NotInvitedPage())
123+
r.Get("/access-denied", handlers.AccessDeniedPage())
123124
r.Get("/signin/verify", handlers.VerifySignInKey(enum.EmailVerificationKindSignIn))
124125
r.Get("/invite/verify", handlers.VerifySignInKey(enum.EmailVerificationKindUserInvitation))
125126
r.Post("/_api/signin/complete", handlers.CompleteSignInProfile())

app/handlers/admin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ func SaveOAuthConfig() web.HandlerFunc {
242242
JSONUserIDPath: action.JSONUserIDPath,
243243
JSONUserNamePath: action.JSONUserNamePath,
244244
JSONUserEmailPath: action.JSONUserEmailPath,
245+
JSONUserRolesPath: action.JSONUserRolesPath,
245246
},
246247
); err != nil {
247248
return c.Failure(err)

app/handlers/oauth.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/getfider/fider/app/pkg/bus"
1818

1919
"github.com/getfider/fider/app"
20+
"github.com/getfider/fider/app/pkg/env"
2021
"github.com/getfider/fider/app/pkg/errors"
2122
"github.com/getfider/fider/app/pkg/jwt"
2223
"github.com/getfider/fider/app/pkg/log"
@@ -91,6 +92,17 @@ func OAuthToken() web.HandlerFunc {
9192
return c.Failure(err)
9293
}
9394

95+
// Check if user has required roles (if OAUTH_ALLOWED_ROLES is configured)
96+
if !hasAllowedRole(oauthUser.Result.Roles) {
97+
log.Warnf(c, "User @{UserID} attempted OAuth login but does not have required role. User roles: @{UserRoles}, Allowed roles: @{AllowedRoles}",
98+
dto.Props{
99+
"UserID": oauthUser.Result.ID,
100+
"UserRoles": oauthUser.Result.Roles,
101+
"AllowedRoles": env.Config.OAuth.AllowedRoles,
102+
})
103+
return c.Redirect("/access-denied")
104+
}
105+
94106
var user *entity.User
95107

96108
userByProvider := &query.GetUserByProvider{Provider: provider, UID: oauthUser.Result.ID}
@@ -264,3 +276,42 @@ func SignInByOAuth() web.HandlerFunc {
264276
return c.Redirect(authURL.Result)
265277
}
266278
}
279+
280+
// hasAllowedRole checks if the user has any of the allowed roles configured in OAUTH_ALLOWED_ROLES
281+
// If OAUTH_ALLOWED_ROLES is not set or empty, all users are allowed (returns true)
282+
// If set, user must have at least one of the specified roles
283+
func hasAllowedRole(userRoles []string) bool {
284+
allowedRolesConfig := strings.TrimSpace(env.Config.OAuth.AllowedRoles)
285+
286+
// If no roles restriction is configured, allow all users
287+
if allowedRolesConfig == "" {
288+
return true
289+
}
290+
291+
// Parse allowed roles from config (semicolon-separated)
292+
allowedRoles := strings.Split(allowedRolesConfig, ";")
293+
allowedRolesMap := make(map[string]bool)
294+
for _, role := range allowedRoles {
295+
role = strings.TrimSpace(role)
296+
if role != "" {
297+
allowedRolesMap[role] = true
298+
}
299+
}
300+
301+
// If no valid roles in config, allow all
302+
if len(allowedRolesMap) == 0 {
303+
return true
304+
}
305+
306+
// Check if user has any of the allowed roles
307+
for _, userRole := range userRoles {
308+
userRole = strings.TrimSpace(userRole)
309+
if allowedRolesMap[userRole] {
310+
return true
311+
}
312+
}
313+
314+
// User doesn't have any of the required roles
315+
return false
316+
}
317+

app/handlers/signin.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ func NotInvitedPage() web.HandlerFunc {
7171
}
7272
}
7373

74+
// AccessDeniedPage renders the access denied page for OAuth role mismatches
75+
func AccessDeniedPage() web.HandlerFunc {
76+
return func(c *web.Context) error {
77+
return c.Page(http.StatusForbidden, web.Props{
78+
Page: "Error/AccessDenied.page",
79+
Title: "Access Denied",
80+
Description: "You do not have the required permissions to access this site.",
81+
})
82+
}
83+
}
84+
7485
// SignInByEmail checks if user exists and sends code only for existing users
7586
func SignInByEmail() web.HandlerFunc {
7687
return func(c *web.Context) error {

app/models/cmd/oauth.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type SaveCustomOAuthConfig struct {
2020
JSONUserIDPath string
2121
JSONUserNamePath string
2222
JSONUserEmailPath string
23+
JSONUserRolesPath string
2324
}
2425

2526
type ParseOAuthRawProfile struct {

app/models/dto/oauth.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ package dto
22

33
//OAuthUserProfile represents an OAuth user profile
44
type OAuthUserProfile struct {
5-
ID string `json:"id"`
6-
Name string `json:"name"`
7-
Email string `json:"email"`
5+
ID string `json:"id"`
6+
Name string `json:"name"`
7+
Email string `json:"email"`
8+
Roles []string `json:"roles"`
89
}
910

1011
//OAuthProviderOption represents an OAuth provider that can be used to authenticate

app/models/entity/oauth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type OAuthConfig struct {
2727
JSONUserIDPath string
2828
JSONUserNamePath string
2929
JSONUserEmailPath string
30+
JSONUserRolesPath string
3031
}
3132

3233
// MarshalJSON returns the JSON encoding of OAuthConfig
@@ -51,5 +52,6 @@ func (o OAuthConfig) MarshalJSON() ([]byte, error) {
5152
"jsonUserIDPath": o.JSONUserIDPath,
5253
"jsonUserNamePath": o.JSONUserNamePath,
5354
"jsonUserEmailPath": o.JSONUserEmailPath,
55+
"jsonUserRolesPath": o.JSONUserRolesPath,
5456
})
5557
}

app/pkg/env/env.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ type config struct {
9999
ClientID string `env:"OAUTH_GITHUB_CLIENTID"`
100100
Secret string `env:"OAUTH_GITHUB_SECRET"`
101101
}
102+
AllowedRoles string `env:"OAUTH_ALLOWED_ROLES"`
102103
}
103104
Email struct {
104105
Type string `env:"EMAIL"` // possible values: smtp, mailgun, awsses

0 commit comments

Comments
 (0)