forked from cesanta/docker_auth
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added OpenID Connect support (cesanta#307)
* Added OIDC support * Changed Dockerfile to multi-stage build * Reset Makefile to be usable with the new Dockerfile * Added page that will show the resulting login credentials * Disabled the go:generate command and the logging of the Version in line 209 since it always made some problems * Fixed a bug that the authentication server could not verify certificates of the OIDC provider * Fixed some bugs * Changed request of refreshing of the session by adding the ClientID and ClientSecret in the HTTP Basic authentication header. Furthermore added some comments. * Changed whole oidc_auth to use coreos/go-oidc package * fixed refreshAccessToken due to missing header * Fixes for Go 1.16 * Fixed Dockerfile * Fixed small thing in loggings * Added example in reference.yml * Delete .idea directory * adapt to go.mod of original repo * Update go.mod * undo stuff of before Co-authored-by: basels <[email protected]> Co-authored-by: techknowlogick <[email protected]>
- Loading branch information
1 parent
9aa1b21
commit cc91cb2
Showing
8 changed files
with
451 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<!doctype html> | ||
|
||
<html> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>Docker Registry Authentication</title> | ||
</head> | ||
|
||
<body> | ||
<div id="panel"> | ||
<p> | ||
<a id="login-with-oidc" href="{{.AuthEndpoint}}?response_type=code&scope=openid%20email&client_id={{.ClientId}}&redirect_uri={{.RedirectURI}}"> | ||
Login with OIDC Provider | ||
</a> | ||
</p> | ||
</div> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<!doctype html> | ||
|
||
<html> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>Docker Registry Authentication</title> | ||
</head> | ||
|
||
<body> | ||
<p class="message"> | ||
You are successfully authenticated for the Docker Registry. | ||
Use the following username and password to login into the registry: | ||
</p> | ||
<hr> | ||
<pre class="command"><span>$ </span>docker login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}</pre> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,352 @@ | ||
/* | ||
Copyright 2015 Cesanta Software Ltd. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
https://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package authn | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"golang.org/x/oauth2" | ||
"html/template" | ||
"io/ioutil" | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
"github.com/coreos/go-oidc/v3/oidc" | ||
|
||
"github.com/cesanta/glog" | ||
|
||
"github.com/cesanta/docker_auth/auth_server/api" | ||
) | ||
|
||
// All configuration options | ||
type OIDCAuthConfig struct { | ||
// --- necessary --- | ||
// URL of the authentication provider. Must be able to serve the /.well-known/openid-configuration | ||
Issuer string `yaml:"issuer,omitempty"` | ||
// URL of the auth server. Has to end with /oidc_auth | ||
RedirectURL string `yaml:"redirect_url,omitempty"` | ||
// ID and secret, priovided by the OIDC provider after registration of the auth server | ||
ClientId string `yaml:"client_id,omitempty"` | ||
ClientSecret string `yaml:"client_secret,omitempty"` | ||
ClientSecretFile string `yaml:"client_secret_file,omitempty"` | ||
// path where the tokendb should be stored within the container | ||
TokenDB string `yaml:"token_db,omitempty"` | ||
// --- optional --- | ||
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"` | ||
} | ||
|
||
// OIDCRefreshTokenResponse is sent by OIDC provider in response to the grant_type=refresh_token request. | ||
type OIDCRefreshTokenResponse struct { | ||
AccessToken string `json:"access_token,omitempty"` | ||
ExpiresIn int64 `json:"expires_in,omitempty"` | ||
TokenType string `json:"token_type,omitempty"` | ||
RefreshToken string `json:"refresh_token,omitempty"` | ||
|
||
// Returned in case of error. | ||
Error string `json:"error,omitempty"` | ||
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 | ||
db TokenDB | ||
client *http.Client | ||
tmpl *template.Template | ||
tmplResult *template.Template | ||
ctx context.Context | ||
provider *oidc.Provider | ||
verifier *oidc.IDTokenVerifier | ||
oauth oauth2.Config | ||
} | ||
|
||
/* | ||
Creates everything necessary for OIDC auth. | ||
*/ | ||
func NewOIDCAuth(c *OIDCAuthConfig) (*OIDCAuth, error) { | ||
db, err := NewTokenDB(c.TokenDB) | ||
if err != nil { | ||
return nil, err | ||
} | ||
glog.Infof("OIDC auth token DB at %s", c.TokenDB) | ||
ctx := context.Background() | ||
oidcAuth, _ := static.ReadFile("data/oidc_auth.tmpl") | ||
oidcAuthResult, _ := static.ReadFile("data/oidc_auth_result.tmpl") | ||
|
||
prov, err := oidc.NewProvider(ctx, c.Issuer) | ||
if err != nil { | ||
return nil, err | ||
} | ||
conf := oauth2.Config{ | ||
ClientID: c.ClientId, | ||
ClientSecret: c.ClientSecret, | ||
Endpoint: prov.Endpoint(), | ||
RedirectURL: c.RedirectURL, | ||
Scopes: []string{oidc.ScopeOpenID, "email"}, | ||
} | ||
return &OIDCAuth{ | ||
config: c, | ||
db: db, | ||
client: &http.Client{Timeout: 10 * time.Second}, | ||
tmpl: template.Must(template.New("oidc_auth").Parse(string(oidcAuth))), | ||
tmplResult: template.Must(template.New("oidc_auth_result").Parse(string(oidcAuthResult))), | ||
ctx: ctx, | ||
provider: prov, | ||
verifier: prov.Verifier(&oidc.Config{ClientID: conf.ClientID}), | ||
oauth: conf, | ||
}, nil | ||
} | ||
|
||
/* | ||
This function will be used by the server if the OIDC auth method is selected. It starts the page for OIDC login or | ||
requests an access token by using the code given by the OIDC provider. | ||
*/ | ||
func (ga *OIDCAuth) DoOIDCAuth(rw http.ResponseWriter, req *http.Request) { | ||
code := req.URL.Query().Get("code") | ||
if code != "" { | ||
ga.doOIDCAuthCreateToken(rw, code) | ||
} else if req.Method == "GET" { | ||
ga.doOIDCAuthPage(rw) | ||
} else { | ||
http.Error(rw, "Invalid auth request", http.StatusBadRequest) | ||
} | ||
} | ||
|
||
/* | ||
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: ga.provider.Endpoint().AuthURL, | ||
RedirectURI: ga.oauth.RedirectURL, | ||
ClientId: ga.oauth.ClientID, | ||
}); err != nil { | ||
http.Error(rw, fmt.Sprintf("Template error: %s", err), http.StatusInternalServerError) | ||
} | ||
} | ||
|
||
/* | ||
Executes tmplResult for the result of the login process. | ||
*/ | ||
func (ga *OIDCAuth) doOIDCAuthResultPage(rw http.ResponseWriter, un string, pw string) { | ||
if err := ga.tmplResult.Execute(rw, struct { | ||
Username, Password, RegistryUrl string | ||
}{ | ||
Username: un, | ||
Password: pw, | ||
RegistryUrl: ga.config.RegistryURL, | ||
}); err != nil { | ||
http.Error(rw, fmt.Sprintf("Template error: %s", err), http.StatusInternalServerError) | ||
} | ||
} | ||
|
||
/* | ||
Requests an OIDC token by using the code that was provided by the OIDC provider. If it was successfull, | ||
the access token and refresh token is used to create a new token for the users mail address, which is taken from the ID | ||
token. | ||
*/ | ||
func (ga *OIDCAuth) doOIDCAuthCreateToken(rw http.ResponseWriter, code string) { | ||
|
||
tok, err := ga.oauth.Exchange(ga.ctx, code) | ||
if err != nil { | ||
http.Error(rw, fmt.Sprintf("Error talking to OIDC auth backend: %s", err), http.StatusInternalServerError) | ||
return | ||
} | ||
rawIdTok, ok := tok.Extra("id_token").(string) | ||
if !ok { | ||
http.Error(rw, "No id_token field in oauth2 token.", http.StatusInternalServerError) | ||
return | ||
} | ||
idTok, err := ga.verifier.Verify(ga.ctx, rawIdTok) | ||
if err != nil { | ||
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) | ||
return | ||
} | ||
if prof.Email == "" { | ||
http.Error(rw, fmt.Sprintf("No mail information given in ID token"), 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()) | ||
|
||
dbVal := &TokenDBValue{ | ||
TokenType: tok.TokenType, | ||
AccessToken: tok.AccessToken, | ||
RefreshToken: tok.RefreshToken, | ||
ValidUntil: tok.Expiry.Add(time.Duration(-30) * time.Second), | ||
} | ||
dp, err := ga.db.StoreToken(prof.Email, 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) | ||
} | ||
|
||
/* | ||
Refreshes the access token of the user. Not usable with all OIDC provider, since not all provide refresh tokens. | ||
*/ | ||
func (ga *OIDCAuth) refreshAccessToken(refreshToken string) (rtr OIDCRefreshTokenResponse, err error) { | ||
|
||
url := ga.provider.Endpoint().TokenURL | ||
pl := strings.NewReader(fmt.Sprintf( | ||
"grant_type=refresh_token&client_id=%s&client_secret=%s&refresh_token=%s", | ||
ga.oauth.ClientID, ga.oauth.ClientSecret, refreshToken)) | ||
req, err := http.NewRequest("POST", url, pl) | ||
if err != nil { | ||
err = fmt.Errorf("could not create refresh request: %s", err) | ||
return | ||
} | ||
req.Header.Add("content-type", "application/x-www-form-urlencoded") | ||
|
||
resp, err := ga.client.Do(req) | ||
if err != nil { | ||
err = fmt.Errorf("error talking to OIDC auth backend: %s", err) | ||
return | ||
} | ||
respStr, _ := ioutil.ReadAll(resp.Body) | ||
glog.V(2).Infof("Refresh token resp: %s", strings.Replace(string(respStr), "\n", " ", -1)) | ||
|
||
err = json.Unmarshal(respStr, &rtr) | ||
if err != nil { | ||
err = fmt.Errorf("error in reading response of refresh request: %s", err) | ||
return | ||
} | ||
if rtr.Error != "" || rtr.ErrorDescription != "" { | ||
err = fmt.Errorf("%s: %s", rtr.Error, rtr.ErrorDescription) | ||
return | ||
} | ||
return rtr, err | ||
} | ||
|
||
/* | ||
In case the DB token is expired, this function uses the refresh token and tries to refresh the access token stored in the | ||
DB. Afterwards, checks if the access token really authenticates the user trying to log in. | ||
*/ | ||
func (ga *OIDCAuth) validateServerToken(user string) (*TokenDBValue, error) { | ||
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 nil, err | ||
} | ||
if v.RefreshToken == "" { | ||
return nil, errors.New("refresh of your session is not possible. Please sign out and sign in again") | ||
} | ||
|
||
glog.V(2).Infof("Refreshing token for %s", user) | ||
rtr, err := ga.refreshAccessToken(v.RefreshToken) | ||
if err != nil { | ||
glog.Warningf("Failed to refresh token for %q: %s", user, err) | ||
return nil, fmt.Errorf("failed to refresh token: %s", err) | ||
} | ||
v.AccessToken = rtr.AccessToken | ||
v.ValidUntil = time.Now().Add(time.Duration(rtr.ExpiresIn-30) * time.Second) | ||
glog.Infof("Refreshed auth token for %s (exp %d)", user, rtr.ExpiresIn) | ||
_, err = ga.db.StoreToken(user, v, false) | ||
if err != nil { | ||
glog.Errorf("Failed to record refreshed token: %s", err) | ||
return nil, fmt.Errorf("failed to record refreshed token: %s", err) | ||
} | ||
tokUser, err := ga.provider.UserInfo(ga.ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: v.AccessToken, | ||
TokenType: v.TokenType, | ||
RefreshToken: v.RefreshToken, | ||
Expiry: v.ValidUntil, | ||
})) | ||
if err != nil { | ||
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) | ||
return nil, fmt.Errorf("found token for wrong user") | ||
} | ||
texp := v.ValidUntil.Sub(time.Now()) | ||
glog.V(1).Infof("Validated OIDC auth token for %s (exp %d)", user, int(texp.Seconds())) | ||
return v, nil | ||
} | ||
|
||
/* | ||
First checks if OIDC token is valid. Then delete the corresponding DB token from the database. The user is now signed out | ||
Not deleted because maybe it will be implemented in the future. | ||
*/ | ||
//func (ga *OIDCAuth) doOIDCAuthSignOut(rw http.ResponseWriter, token string) { | ||
// // Authenticate web user. | ||
// ui, err := ga.validateIDToken(token) | ||
// if err != nil || ui == ""{ | ||
// http.Error(rw, fmt.Sprintf("Could not verify user token: %s", err), http.StatusBadRequest) | ||
// return | ||
// } | ||
// err = ga.db.DeleteToken(ui) | ||
// if err != nil { | ||
// glog.Error(err) | ||
// } | ||
// fmt.Fprint(rw, "signed out") | ||
//} | ||
|
||
/* | ||
Called by server. Authenticates user with credentials that were given in the docker login command. If the token in the | ||
DB is expired, the OIDC access token is validated and, if possible, refreshed. | ||
*/ | ||
func (ga *OIDCAuth) Authenticate(user string, password api.PasswordString) (bool, api.Labels, error) { | ||
err := ga.db.ValidateToken(user, password) | ||
if err == ExpiredToken { | ||
_, err = ga.validateServerToken(user) | ||
if err != nil { | ||
return false, nil, err | ||
} | ||
} else if err != nil { | ||
return false, nil, err | ||
} | ||
return true, nil, nil | ||
} | ||
|
||
func (ga *OIDCAuth) Stop() { | ||
err := ga.db.Close() | ||
if err != nil { | ||
glog.Info("Problems at closing the token DB") | ||
} else { | ||
glog.Info("Token DB closed") | ||
} | ||
} | ||
|
||
func (ga *OIDCAuth) Name() string { | ||
return "OpenID Connect" | ||
} |
Oops, something went wrong.