diff --git a/README.md b/README.md index 6613580..9711eb6 100644 --- a/README.md +++ b/README.md @@ -74,3 +74,9 @@ clients: If you do not want to provide a plaintext secret, you can also provide the secret as an already hashed bcrypt2 value + +#### Group ACLs + +In case you want your client to be only available for members of a certain +group, you can populate the `allowGroups` or `denyGroups` fields in the client +config. This will either allow or deny access on a client basis. diff --git a/auth/auth.go b/auth/auth.go index 8aeb3f1..635574a 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -28,6 +28,13 @@ type OIDCProvider struct { discourseSecret string privateKey *rsa.PrivateKey } + +type DistrustClient struct { + fosite.DefaultClient + AllowGroups []string + DenyGroups []string +} + type InFlightRequest struct { Nonce int Ar fosite.AuthorizeRequester diff --git a/auth/handlers.go b/auth/handlers.go index f31f077..d0ce917 100644 --- a/auth/handlers.go +++ b/auth/handlers.go @@ -2,9 +2,13 @@ package auth import ( "encoding/json" + "errors" + "fmt" "math/rand" "net/http" + "net/url" "strconv" + "strings" "time" "github.com/google/uuid" @@ -84,6 +88,17 @@ func (o *OIDCProvider) callbackEndpoint(rw http.ResponseWriter, req *http.Reques Int("nonce", nonce). Msg("parsed user data") + switch client := session.Ar.GetClient().(type) { + case *DistrustClient: + log.Debug().Str("client", client.GetID()).Msg("distrust client found, performing additonal validation") + err := validateGroups(client, values) + if err != nil { + log.Warn().Err(err).Msg("group validation failed") + fmt.Fprintf(rw, "You are not allowed to access this application: %s", err.Error()) + return + } + } + // since scopes do not work with discourse, we simply grant the openid scope session.Ar.GrantScope("openid") @@ -226,3 +241,25 @@ func (o *OIDCProvider) getAuthRoot(req *http.Request) string { aroot := scheme + "://" + req.Host + o.root return aroot } + +func validateGroups(client *DistrustClient, values url.Values) error { + userGroups := values.Get("groups") + groupMap := make(map[string]bool) + for _, g := range strings.Split(userGroups, ",") { + groupMap[g] = true + } + for _, allowed := range client.AllowGroups { + if groupMap[allowed] { + return nil + } + } + if len(client.AllowGroups) != 0 { + return errors.New("user is not in allowed groups for this client") + } + for _, denied := range client.DenyGroups { + if groupMap[denied] { + return errors.New("access is denied for user in group " + denied) + } + } + return nil +} diff --git a/distrust.example.yml b/distrust.example.yml index 3f0e3e4..0935db7 100644 --- a/distrust.example.yml +++ b/distrust.example.yml @@ -39,5 +39,6 @@ oidc: clients: test: secret: foobar + allowGroups: ['team'] redirectURIs: - 'https://openidconnect.net/callback' diff --git a/main.go b/main.go index 62bae70..f7e31df 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,8 @@ import ( type clientConfig struct { Secret string RedirectURIs []string + AllowGroups []string + DenyGroups []string } func main() { @@ -102,12 +104,20 @@ func toFositeClients(clients map[string]clientConfig) map[string]fosite.Client { hs, _ = bcrypt.GenerateFromPassword(hs, bcrypt.DefaultCost) } - r[k] = &fosite.DefaultClient{ - Secret: hs, - RedirectURIs: v.RedirectURIs, - ResponseTypes: []string{"id_token", "code", "token", "id_token token", "code id_token", "code token", "code id_token token"}, - GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}, - Scopes: []string{"openid", "profile", "email"}, + r[k] = &auth.DistrustClient{ + DefaultClient: fosite.DefaultClient{ + ID: k, + Secret: hs, + RedirectURIs: v.RedirectURIs, + ResponseTypes: []string{"id_token", "code", "token", "id_token token", "code id_token", "code token", "code id_token token"}, + GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}, + Scopes: []string{"openid", "profile", "email"}, + }, + AllowGroups: v.AllowGroups, + DenyGroups: v.DenyGroups, + } + if len(v.AllowGroups) != 0 && len(v.DenyGroups) != 0 { + log.Warn().Str("client", k).Msg("allow and deny group options are set. allow groups will be used") } } return r