Skip to content

Commit

Permalink
Add more configuration options for OIDC auth (#344)
Browse files Browse the repository at this point in the history
* Make user claim configurable and add labels

* Add `scopes` option to OIDC

* Update OIDC template with `scope` from config

* Return labels from token
  • Loading branch information
devon-mar committed Mar 1, 2023
1 parent 1e44729 commit 3a7249e
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 23 deletions.
2 changes: 1 addition & 1 deletion auth_server/authn/data/oidc_auth.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<body>
<div id="panel">
<p>
<a id="login-with-oidc" href="{{.AuthEndpoint}}?response_type=code&scope=openid%20email&client_id={{.ClientId}}&redirect_uri={{.RedirectURI}}">
<a id="login-with-oidc" href="{{.AuthEndpoint}}?response_type=code&scope={{.Scope}}&client_id={{.ClientId}}&redirect_uri={{.RedirectURI}}">
Login with OIDC Provider
</a>
</p>
Expand Down
76 changes: 54 additions & 22 deletions auth_server/authn/oidc_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ import (
"encoding/json"
"errors"
"fmt"
"golang.org/x/oauth2"
"html/template"
"io/ioutil"
"net/http"
"strings"
"time"

"golang.org/x/oauth2"

"github.com/coreos/go-oidc/v3/oidc"

"github.com/cesanta/glog"
Expand All @@ -52,6 +53,14 @@ type OIDCAuthConfig struct {
HTTPTimeout int `yaml:"http_timeout,omitempty"`
// the URL of the docker registry. Used to generate a full docker login command after authentication
RegistryURL string `yaml:"registry_url,omitempty"`
// --- optional ---
// String claim to use for the username
UserClaim string `yaml:"user_claim,omitempty"`
// --- optional ---
// []string to add as labels.
LabelsClaims []string `yaml:"labels_claims,omitempty"`
// --- optional ---
Scopes []string `yaml:"scopes,omitempty"`
}

// OIDCRefreshTokenResponse is sent by OIDC provider in response to the grant_type=refresh_token request.
Expand All @@ -66,14 +75,6 @@ type OIDCRefreshTokenResponse struct {
ErrorDescription string `json:"error_description,omitempty"`
}

// ProfileResponse is sent by the /userinfo endpoint or contained in the ID token.
// We use it to validate access token and (re)verify the email address associated with it.
type OIDCProfileResponse struct {
Email string `json:"email,omitempty"`
VerifiedEmail bool `json:"verified_email,omitempty"`
// There are more fields, but we only need email.
}

// The specific OIDC authenticator
type OIDCAuth struct {
config *OIDCAuthConfig
Expand Down Expand Up @@ -109,7 +110,7 @@ func NewOIDCAuth(c *OIDCAuthConfig) (*OIDCAuth, error) {
ClientSecret: c.ClientSecret,
Endpoint: prov.Endpoint(),
RedirectURL: c.RedirectURL,
Scopes: []string{oidc.ScopeOpenID, "email"},
Scopes: c.Scopes,
}
return &OIDCAuth{
config: c,
Expand Down Expand Up @@ -144,11 +145,12 @@ Executes tmpl for the OIDC login page.
*/
func (ga *OIDCAuth) doOIDCAuthPage(rw http.ResponseWriter) {
if err := ga.tmpl.Execute(rw, struct {
AuthEndpoint, RedirectURI, ClientId string
AuthEndpoint, RedirectURI, ClientId, Scope string
}{
AuthEndpoint: ga.provider.Endpoint().AuthURL,
RedirectURI: ga.oauth.RedirectURL,
ClientId: ga.oauth.ClientID,
Scope: strings.Join(ga.config.Scopes, " "),
}); err != nil {
http.Error(rw, fmt.Sprintf("Template error: %s", err), http.StatusInternalServerError)
}
Expand Down Expand Up @@ -191,32 +193,47 @@ func (ga *OIDCAuth) doOIDCAuthCreateToken(rw http.ResponseWriter, code string) {
http.Error(rw, fmt.Sprintf("Failed to verify ID token: %s", err), http.StatusInternalServerError)
return
}
var prof OIDCProfileResponse
if err := idTok.Claims(&prof); err != nil {
http.Error(rw, fmt.Sprintf("Failed to get mail information from ID token: %s", err), http.StatusInternalServerError)
var claims map[string]interface{}
if err := idTok.Claims(&claims); err != nil {
http.Error(rw, fmt.Sprintf("Failed to get claims from ID token: %s", err), http.StatusInternalServerError)
return
}
if prof.Email == "" {
http.Error(rw, fmt.Sprintf("No mail information given in ID token"), http.StatusInternalServerError)
username, _ := claims[ga.config.UserClaim].(string)
if username == "" {
http.Error(rw, fmt.Sprintf("No %q claim in ID token", ga.config.UserClaim), http.StatusInternalServerError)
return
}

glog.V(2).Infof("New OIDC auth token for %s (Current time: %s, expiration time: %s)", prof.Email, time.Now().String(), tok.Expiry.String())
glog.V(2).Infof("New OIDC auth token for %s (Current time: %s, expiration time: %s)", username, time.Now().String(), tok.Expiry.String())

dbVal := &TokenDBValue{
TokenType: tok.TokenType,
AccessToken: tok.AccessToken,
RefreshToken: tok.RefreshToken,
ValidUntil: tok.Expiry.Add(time.Duration(-30) * time.Second),
Labels: ga.getLabels(claims),
}
dp, err := ga.db.StoreToken(prof.Email, dbVal, true)
dp, err := ga.db.StoreToken(username, dbVal, true)
if err != nil {
glog.Errorf("Failed to record server token: %s", err)
http.Error(rw, "Failed to record server token: %s", http.StatusInternalServerError)
return
}

ga.doOIDCAuthResultPage(rw, prof.Email, dp)
ga.doOIDCAuthResultPage(rw, username, dp)
}

func (ga *OIDCAuth) getLabels(claims map[string]interface{}) api.Labels {
labels := make(api.Labels, len(ga.config.LabelsClaims))
for _, claim := range ga.config.LabelsClaims {
values, _ := claims[claim].([]interface{})
for _, v := range values {
if str, _ := v.(string); str != "" {
labels[claim] = append(labels[claim], str)
}
}
}
return labels
}

/*
Expand Down Expand Up @@ -294,8 +311,15 @@ func (ga *OIDCAuth) validateServerToken(user string) (*TokenDBValue, error) {
glog.Warningf("Token for %q failed validation: %s", user, err)
return nil, fmt.Errorf("server token invalid: %s", err)
}
if tokUser.Email != user {
glog.Errorf("token for wrong user: expected %s, found %s", user, tokUser.Email)

var claims map[string]interface{}
if err := tokUser.Claims(&claims); err != nil {
glog.Errorf("error retrieving claims: %v", err)
return nil, fmt.Errorf("error retrieving claims: %w", err)
}
claimUsername, _ := claims[ga.config.UserClaim].(string)
if claimUsername != user {
glog.Errorf("token for wrong user: expected %s, found %s", user, claimUsername)
return nil, fmt.Errorf("found token for wrong user")
}
texp := v.ValidUntil.Sub(time.Now())
Expand Down Expand Up @@ -335,7 +359,15 @@ func (ga *OIDCAuth) Authenticate(user string, password api.PasswordString) (bool
} else if err != nil {
return false, nil, err
}
return true, nil, nil

v, err := ga.db.GetValue(user)
if err != nil || v == nil {
if err == nil {
err = errors.New("no db value, please sign out and sign in again")
}
return false, nil, err
}
return true, v.Labels, err
}

func (ga *OIDCAuth) Stop() {
Expand Down
6 changes: 6 additions & 0 deletions auth_server/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,12 @@ func validate(c *Config) error {
if oidc.HTTPTimeout <= 0 {
oidc.HTTPTimeout = 10
}
if oidc.UserClaim == "" {
oidc.UserClaim = "email"
}
if oidc.Scopes == nil {
oidc.Scopes = []string{"openid", "email"}
}
}
if glab := c.GitlabAuth; glab != nil {
if glab.ClientSecretFile != "" {
Expand Down
11 changes: 11 additions & 0 deletions examples/reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,17 @@ oidc_auth:
http_timeout: 10
# the url of the registry where you want to login. Is used to present the full docker login command.
registry_url: "url_of_my_beautiful_docker_registry"
# The claim to use for the username.
# Default: email
user_claim: email
# String array claims that will be used as labels.
label_claims:
- groups
# Default: [openid, email]
scopes:
- openid
- email


# Gitlab authentication.
# ==! NB: DO NOT ENTER YOUR Gitlab PASSWORD AT "docker login". IT WILL NOT WORK.
Expand Down

0 comments on commit 3a7249e

Please sign in to comment.