Check nonce of ID token on authentication (#112)

This commit is contained in:
Hidetake Iwata
2019-07-10 09:59:03 +09:00
committed by GitHub
parent 4138991339
commit 79d8056c35
5 changed files with 90 additions and 63 deletions

View File

@@ -2,6 +2,8 @@ package oidc
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"net/http"
"time"
@@ -74,11 +76,15 @@ func (c *client) AuthenticateByCode(ctx context.Context, in adaptors.OIDCAuthent
if c.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
}
nonce, err := newNonce()
if err != nil {
return nil, xerrors.Errorf("could not generate a nonce parameter")
}
config := oauth2cli.Config{
OAuth2Config: c.oauth2Config,
LocalServerPort: in.LocalServerPort,
SkipOpenBrowser: in.SkipOpenBrowser,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline, oidc.Nonce(nonce)},
ShowLocalServerURL: in.ShowLocalServerURL.ShowLocalServerURL,
}
token, err := oauth2cli.GetToken(ctx, config)
@@ -94,6 +100,9 @@ func (c *client) AuthenticateByCode(ctx context.Context, in adaptors.OIDCAuthent
if err != nil {
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
if verifiedIDToken.Nonce != nonce {
return nil, xerrors.Errorf("nonce of ID token did not match (want %s but was %s)", nonce, verifiedIDToken.Nonce)
}
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
c.logger.Debugf(1, "incomplete claims of the ID token: %w", err)
@@ -106,6 +115,14 @@ func (c *client) AuthenticateByCode(ctx context.Context, in adaptors.OIDCAuthent
}, nil
}
func newNonce() (string, error) {
var n uint64
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
return "", xerrors.Errorf("error while reading random: %w", err)
}
return fmt.Sprintf("%x", n), nil
}
// AuthenticateByPassword performs the resource owner password credentials flow.
func (c *client) AuthenticateByPassword(ctx context.Context, in adaptors.OIDCAuthenticateByPasswordIn) (*adaptors.OIDCAuthenticateOut, error) {
if c.httpClient != nil {

View File

@@ -30,7 +30,6 @@ import (
//
func TestCmd_Run(t *testing.T) {
timeout := 1 * time.Second
tokenExpiry := time.Now().Add(time.Hour)
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
@@ -42,12 +41,8 @@ func TestCmd_Run(t *testing.T) {
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, tokenExpiry)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticateCode("openid").Return("YOUR_AUTH_CODE", nil)
service.EXPECT().Exchange("YOUR_AUTH_CODE").
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
defer os.Remove(kubeConfigFilename)
@@ -71,7 +66,7 @@ func TestCmd_Run(t *testing.T) {
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, tokenExpiry)
idToken := newIDToken(t, serverURL, "", time.Hour)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticatePassword("USER", "PASS", "openid").
@@ -97,12 +92,8 @@ func TestCmd_Run(t *testing.T) {
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, tokenExpiry)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticateCode("openid").Return("YOUR_AUTH_CODE", nil)
service.EXPECT().Exchange("YOUR_AUTH_CODE").
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
defer os.Remove(kubeConfigFilename)
@@ -128,12 +119,8 @@ func TestCmd_Run(t *testing.T) {
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, tokenExpiry)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticateCode("profile groups openid").Return("YOUR_AUTH_CODE", nil)
service.EXPECT().Exchange("YOUR_AUTH_CODE").
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "profile groups openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
@@ -160,12 +147,8 @@ func TestCmd_Run(t *testing.T) {
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, tokenExpiry)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticateCode("openid").Return("YOUR_AUTH_CODE", nil)
service.EXPECT().Exchange("YOUR_AUTH_CODE").
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
@@ -192,12 +175,8 @@ func TestCmd_Run(t *testing.T) {
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, tokenExpiry)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticateCode("openid").Return("YOUR_AUTH_CODE", nil)
service.EXPECT().Exchange("YOUR_AUTH_CODE").
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
@@ -224,7 +203,7 @@ func TestCmd_Run(t *testing.T) {
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, tokenExpiry)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", time.Hour)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
@@ -252,7 +231,7 @@ func TestCmd_Run(t *testing.T) {
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, tokenExpiry)
idToken := newIDToken(t, serverURL, "YOUR_NONCE", time.Hour)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
@@ -260,7 +239,7 @@ func TestCmd_Run(t *testing.T) {
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, time.Now().Add(-time.Hour)), // expired
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", -time.Hour), // expired
RefreshToken: "VALID_REFRESH_TOKEN",
})
defer os.Remove(kubeConfigFilename)
@@ -282,19 +261,15 @@ func TestCmd_Run(t *testing.T) {
service := mock_idp.NewMockService(ctrl)
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverURL, tokenExpiry)
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
MaxTimes(2) // package oauth2 will retry refreshing the token
service.EXPECT().AuthenticateCode("openid").Return("YOUR_AUTH_CODE", nil)
service.EXPECT().Exchange("YOUR_AUTH_CODE").
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverURL,
IDToken: newIDToken(t, serverURL, time.Now().Add(-time.Hour)), // expired
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", -time.Hour), // expired
RefreshToken: "EXPIRED_REFRESH_TOKEN",
})
defer os.Remove(kubeConfigFilename)
@@ -308,19 +283,21 @@ func TestCmd_Run(t *testing.T) {
})
}
func newIDToken(t *testing.T, issuer string, expiry time.Time) string {
func newIDToken(t *testing.T, issuer, nonce string, expiration time.Duration) string {
t.Helper()
var claims struct {
jwt.StandardClaims
Nonce string `json:"nonce"`
Groups []string `json:"groups"`
}
claims.StandardClaims = jwt.StandardClaims{
Issuer: issuer,
Audience: "kubernetes",
ExpiresAt: expiry.Unix(),
Subject: "SUBJECT",
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(expiration).Unix(),
}
claims.Nonce = nonce
claims.Groups = []string{"admin", "users"}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
s, err := token.SignedString(keys.JWSKeyPair)
@@ -330,6 +307,22 @@ func newIDToken(t *testing.T, issuer string, expiry time.Time) string {
return s
}
func setupMockIDPForCodeFlow(t *testing.T, service *mock_idp.MockService, serverURL, scope string, idToken *string) {
var nonce string
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
service.EXPECT().AuthenticateCode(scope, gomock.Any()).
DoAndReturn(func(_, gotNonce string) (string, error) {
nonce = gotNonce
return "YOUR_AUTH_CODE", nil
})
service.EXPECT().Exchange("YOUR_AUTH_CODE").
DoAndReturn(func(string) (*idp.TokenResponse, error) {
*idToken = newIDToken(t, serverURL, nonce, time.Hour)
return idp.NewTokenResponse(*idToken, "YOUR_REFRESH_TOKEN"), nil
})
}
func runCmd(t *testing.T, ctx context.Context, s usecases.LoginShowLocalServerURL, args ...string) {
t.Helper()
cmd := di.NewCmdWith(logger.New(t), s)

View File

@@ -14,18 +14,23 @@ func NewHandler(t *testing.T, service Service) *Handler {
return &Handler{t, service}
}
// Handler provides a HTTP handler for the identity provider of OpenID Connect.
// You need to implement the Service interface.
// Note that this skips some security checks and is only for testing.
type Handler struct {
t *testing.T
service Service
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := h.serveHTTP(w, r)
wr := &responseWriterRecorder{w, 200}
err := h.serveHTTP(wr, r)
if err == nil {
h.t.Logf("%d %s %s", wr.statusCode, r.Method, r.RequestURI)
return
}
if errResp := new(ErrorResponse); xerrors.As(err, &errResp) {
h.t.Logf("idp/handler: 400 Bad Request: %+v", errResp)
h.t.Logf("400 %s %s: %s", r.Method, r.RequestURI, err)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(400)
e := json.NewEncoder(w)
@@ -34,14 +39,23 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
return
}
h.t.Errorf("idp/handler: 500 Server Error: %s", err)
h.t.Logf("500 %s %s: %s", r.Method, r.RequestURI, err)
http.Error(w, err.Error(), 500)
}
type responseWriterRecorder struct {
http.ResponseWriter
statusCode int
}
func (w *responseWriterRecorder) WriteHeader(statusCode int) {
w.ResponseWriter.WriteHeader(statusCode)
w.statusCode = statusCode
}
func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
h.t.Logf("idp/handler: %s %s", m, r.RequestURI)
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
discoveryResponse := h.service.Discovery()
@@ -58,11 +72,11 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
return xerrors.Errorf("could not render json: %w", err)
}
case m == "GET" && p == "/auth":
// Authentication Response
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse
// 3.1.2.1. Authentication Request
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
q := r.URL.Query()
redirectURI, scope, state := q.Get("redirect_uri"), q.Get("scope"), q.Get("state")
code, err := h.service.AuthenticateCode(scope)
redirectURI, scope, state, nonce := q.Get("redirect_uri"), q.Get("scope"), q.Get("state"), q.Get("nonce")
code, err := h.service.AuthenticateCode(scope, nonce)
if err != nil {
return xerrors.Errorf("authentication error: %w", err)
}
@@ -76,8 +90,7 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
switch grantType {
case "authorization_code":
// 3.1.3.1. Token Request
// 3.1.3.3. Successful Token Response
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
// https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
code := r.Form.Get("code")
tokenResponse, err := h.service.Exchange(code)
if err != nil {
@@ -89,7 +102,7 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
return xerrors.Errorf("could not render json: %w", err)
}
case "password":
// Token Response
// 4.3. Resource Owner Password Credentials Grant
// https://tools.ietf.org/html/rfc6749#section-4.3
username, password, scope := r.Form.Get("username"), r.Form.Get("password"), r.Form.Get("scope")
tokenResponse, err := h.service.AuthenticatePassword(username, password, scope)
@@ -103,7 +116,6 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
}
case "refresh_token":
// 12.1. Refresh Request
// 12.2. Successful Refresh Response
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
refreshToken := r.Form.Get("refresh_token")
tokenResponse, err := h.service.Refresh(refreshToken)
@@ -116,7 +128,12 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
return xerrors.Errorf("could not render json: %w", err)
}
default:
return xerrors.Errorf("invalid grant_type: %s", grantType)
// 5.2. Error Response
// https://tools.ietf.org/html/rfc6749#section-5.2
return &ErrorResponse{
Code: "invalid_grant",
Description: fmt.Sprintf("unknown grant_type %s", grantType),
}
}
default:
http.NotFound(w, r)

View File

@@ -34,16 +34,16 @@ func (m *MockService) EXPECT() *MockServiceMockRecorder {
}
// AuthenticateCode mocks base method
func (m *MockService) AuthenticateCode(arg0 string) (string, error) {
ret := m.ctrl.Call(m, "AuthenticateCode", arg0)
func (m *MockService) AuthenticateCode(arg0, arg1 string) (string, error) {
ret := m.ctrl.Call(m, "AuthenticateCode", arg0, arg1)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateCode indicates an expected call of AuthenticateCode
func (mr *MockServiceMockRecorder) AuthenticateCode(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateCode", reflect.TypeOf((*MockService)(nil).AuthenticateCode), arg0)
func (mr *MockServiceMockRecorder) AuthenticateCode(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateCode", reflect.TypeOf((*MockService)(nil).AuthenticateCode), arg0, arg1)
}
// AuthenticatePassword mocks base method

View File

@@ -16,7 +16,7 @@ import (
type Service interface {
Discovery() *DiscoveryResponse
GetCertificates() *CertificatesResponse
AuthenticateCode(scope string) (code string, err error)
AuthenticateCode(scope, nonce string) (code string, err error)
Exchange(code string) (*TokenResponse, error)
AuthenticatePassword(username, password, scope string) (*TokenResponse, error)
Refresh(refreshToken string) (*TokenResponse, error)