Merge pull request #757 from ericchiang/constant-refresh-tokens

*: update refresh tokens instead of deleting and creating another
This commit is contained in:
Eric Chiang 2017-01-11 12:09:39 -08:00 committed by GitHub
commit 3c247db00a
15 changed files with 405 additions and 100 deletions

View file

@ -55,7 +55,7 @@ fmt:
@go fmt $(shell go list ./... | grep -v '/vendor/') @go fmt $(shell go list ./... | grep -v '/vendor/')
lint: lint:
@for package in $(shell go list ./... | grep -v '/vendor/' | grep -v '/api'); do \ @for package in $(shell go list ./... | grep -v '/vendor/' | grep -v '/api' | grep -v '/server/internal'); do \
golint -set_exit_status $$package $$i || exit 1; \ golint -set_exit_status $$package $$i || exit 1; \
done done
@ -81,12 +81,15 @@ aci: clean-release _output/bin/dex _output/images/library-alpine-3.4.aci
docker-image: clean-release _output/bin/dex docker-image: clean-release _output/bin/dex
@sudo docker build -t $(DOCKER_IMAGE) . @sudo docker build -t $(DOCKER_IMAGE) .
.PHONY: grpc .PHONY: proto
grpc: api/api.pb.go proto: api/api.pb.go server/internal/types.pb.go
api/api.pb.go: api/api.proto bin/protoc bin/protoc-gen-go api/api.pb.go: api/api.proto bin/protoc bin/protoc-gen-go
@protoc --go_out=plugins=grpc:. api/*.proto @protoc --go_out=plugins=grpc:. api/*.proto
server/internal/types.pb.go: server/internal/types.proto bin/protoc bin/protoc-gen-go
@protoc --go_out=. server/internal/*.proto
bin/protoc: scripts/get-protoc bin/protoc: scripts/get-protoc
@./scripts/get-protoc bin/protoc @./scripts/get-protoc bin/protoc

View file

@ -241,7 +241,7 @@ func (a *app) handleLogin(w http.ResponseWriter, r *http.Request) {
authCodeURL := "" authCodeURL := ""
scopes = append(scopes, "openid", "profile", "email") scopes = append(scopes, "openid", "profile", "email")
if r.FormValue("offline_acecss") != "yes" { if r.FormValue("offline_access") != "yes" {
authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState) authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState)
} else if a.offlineAsScope { } else if a.offlineAsScope {
scopes = append(scopes, "offline_access") scopes = append(scopes, "offline_access")
@ -254,34 +254,42 @@ func (a *app) handleLogin(w http.ResponseWriter, r *http.Request) {
} }
func (a *app) handleCallback(w http.ResponseWriter, r *http.Request) { func (a *app) handleCallback(w http.ResponseWriter, r *http.Request) {
if errMsg := r.FormValue("error"); errMsg != "" {
http.Error(w, errMsg+": "+r.FormValue("error_description"), http.StatusBadRequest)
return
}
if state := r.FormValue("state"); state != exampleAppState {
http.Error(w, fmt.Sprintf("expected state %q got %q", exampleAppState, state), http.StatusBadRequest)
return
}
code := r.FormValue("code")
refresh := r.FormValue("refresh_token")
var ( var (
err error err error
token *oauth2.Token token *oauth2.Token
) )
oauth2Config := a.oauth2Config(nil) oauth2Config := a.oauth2Config(nil)
switch { switch r.Method {
case code != "": case "GET":
// Authorization redirect callback from OAuth2 auth flow.
if errMsg := r.FormValue("error"); errMsg != "" {
http.Error(w, errMsg+": "+r.FormValue("error_description"), http.StatusBadRequest)
return
}
code := r.FormValue("code")
if code == "" {
http.Error(w, fmt.Sprintf("no code in request: %q", r.Form), http.StatusBadRequest)
return
}
if state := r.FormValue("state"); state != exampleAppState {
http.Error(w, fmt.Sprintf("expected state %q got %q", exampleAppState, state), http.StatusBadRequest)
return
}
token, err = oauth2Config.Exchange(a.ctx, code) token, err = oauth2Config.Exchange(a.ctx, code)
case refresh != "": case "POST":
// Form request from frontend to refresh a token.
refresh := r.FormValue("refresh_token")
if refresh == "" {
http.Error(w, fmt.Sprintf("no refresh_token in request: %q", r.Form), http.StatusBadRequest)
return
}
t := &oauth2.Token{ t := &oauth2.Token{
RefreshToken: refresh, RefreshToken: refresh,
Expiry: time.Now().Add(-time.Hour), Expiry: time.Now().Add(-time.Hour),
} }
token, err = oauth2Config.TokenSource(r.Context(), t).Token() token, err = oauth2Config.TokenSource(r.Context(), t).Token()
default: default:
http.Error(w, fmt.Sprintf("no code in request: %q", r.Form), http.StatusBadRequest) http.Error(w, fmt.Sprintf("method not implemented: %s", r.Method), http.StatusBadRequest)
return return
} }

View file

@ -8,7 +8,7 @@ import (
var indexTmpl = template.Must(template.New("index.html").Parse(`<html> var indexTmpl = template.Must(template.New("index.html").Parse(`<html>
<body> <body>
<form action="/login"> <form action="/login" method="post">
<p> <p>
Authenticate for:<input type="text" name="cross_client" placeholder="list of client-ids"> Authenticate for:<input type="text" name="cross_client" placeholder="list of client-ids">
</p> </p>
@ -50,8 +50,13 @@ pre {
<body> <body>
<p> Token: <pre><code>{{ .IDToken }}</code></pre></p> <p> Token: <pre><code>{{ .IDToken }}</code></pre></p>
<p> Claims: <pre><code>{{ .Claims }}</code></pre></p> <p> Claims: <pre><code>{{ .Claims }}</code></pre></p>
{{ if .RefreshToken }}
<p> Refresh Token: <pre><code>{{ .RefreshToken }}</code></pre></p> <p> Refresh Token: <pre><code>{{ .RefreshToken }}</code></pre></p>
<p><a href="{{ .RedirectURL }}?refresh_token={{ .RefreshToken }}">Redeem refresh token</a><p> <form action="{{ .RedirectURL }}" method="post">
<input type="hidden" name="refresh_token" value="{{ .RefreshToken }}">
<input type="submit" value="Redeem refresh token">
</form>
{{ end }}
</body> </body>
</html> </html>
`)) `))

View file

@ -2,6 +2,7 @@ package server
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@ -16,6 +17,7 @@ import (
jose "gopkg.in/square/go-jose.v2" jose "gopkg.in/square/go-jose.v2"
"github.com/coreos/dex/connector" "github.com/coreos/dex/connector"
"github.com/coreos/dex/server/internal"
"github.com/coreos/dex/storage" "github.com/coreos/dex/storage"
) )
@ -645,20 +647,32 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
var refreshToken string var refreshToken string
if reqRefresh { if reqRefresh {
refresh := storage.RefreshToken{ refresh := storage.RefreshToken{
RefreshToken: storage.NewID(), ID: storage.NewID(),
Token: storage.NewID(),
ClientID: authCode.ClientID, ClientID: authCode.ClientID,
ConnectorID: authCode.ConnectorID, ConnectorID: authCode.ConnectorID,
Scopes: authCode.Scopes, Scopes: authCode.Scopes,
Claims: authCode.Claims, Claims: authCode.Claims,
Nonce: authCode.Nonce, Nonce: authCode.Nonce,
ConnectorData: authCode.ConnectorData, ConnectorData: authCode.ConnectorData,
CreatedAt: s.now(),
LastUsed: s.now(),
} }
token := &internal.RefreshToken{
RefreshId: refresh.ID,
Token: refresh.Token,
}
if refreshToken, err = internal.Marshal(token); err != nil {
s.logger.Errorf("failed to marshal refresh token: %v", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return
}
if err := s.storage.CreateRefresh(refresh); err != nil { if err := s.storage.CreateRefresh(refresh); err != nil {
s.logger.Errorf("failed to create refresh token: %v", err) s.logger.Errorf("failed to create refresh token: %v", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return return
} }
refreshToken = refresh.RefreshToken
} }
s.writeAccessToken(w, idToken, refreshToken, expiry) s.writeAccessToken(w, idToken, refreshToken, expiry)
} }
@ -672,14 +686,35 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
return return
} }
refresh, err := s.storage.GetRefresh(code) token := new(internal.RefreshToken)
if err != nil || refresh.ClientID != client.ID { if err := internal.Unmarshal(code, token); err != nil {
if err != storage.ErrNotFound { // For backward compatibility, assume the refresh_token is a raw refresh token ID
s.logger.Errorf("failed to get auth code: %v", err) // if it fails to decode.
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) //
} else { // Because refresh_token values that aren't unmarshable were generated by servers
s.tokenErrHelper(w, errInvalidRequest, "Refresh token is invalid or has already been claimed by another client.", http.StatusBadRequest) // that don't have a Token value, we'll still reject any attempts to claim a
// refresh_token twice.
token = &internal.RefreshToken{RefreshId: code, Token: ""}
} }
refresh, err := s.storage.GetRefresh(token.RefreshId)
if err != nil {
s.logger.Errorf("failed to get refresh token: %v", err)
if err == storage.ErrNotFound {
s.tokenErrHelper(w, errInvalidRequest, "Refresh token is invalid or has already been claimed by another client.", http.StatusBadRequest)
} else {
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
}
return
}
if refresh.ClientID != client.ID {
s.logger.Errorf("client %s trying to claim token for client %s", client.ID, refresh.ClientID)
s.tokenErrHelper(w, errInvalidRequest, "Refresh token is invalid or has already been claimed by another client.", http.StatusBadRequest)
return
}
if refresh.Token != token.Token {
s.logger.Errorf("refresh token with id %s claimed twice", refresh.ID)
s.tokenErrHelper(w, errInvalidRequest, "Refresh token is invalid or has already been claimed by another client.", http.StatusBadRequest)
return return
} }
@ -720,13 +755,6 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return return
} }
// Can the connector refresh the identity? If so, attempt to refresh the data
// in the connector.
//
// TODO(ericchiang): We may want a strict mode where connectors that don't implement
// this interface can't perform refreshing.
if refreshConn, ok := conn.Connector.(connector.RefreshConnector); ok {
ident := connector.Identity{ ident := connector.Identity{
UserID: refresh.Claims.UserID, UserID: refresh.Claims.UserID,
Username: refresh.Claims.Username, Username: refresh.Claims.Username,
@ -735,44 +763,70 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
Groups: refresh.Claims.Groups, Groups: refresh.Claims.Groups,
ConnectorData: refresh.ConnectorData, ConnectorData: refresh.ConnectorData,
} }
ident, err := refreshConn.Refresh(r.Context(), parseScopes(scopes), ident)
// Can the connector refresh the identity? If so, attempt to refresh the data
// in the connector.
//
// TODO(ericchiang): We may want a strict mode where connectors that don't implement
// this interface can't perform refreshing.
if refreshConn, ok := conn.Connector.(connector.RefreshConnector); ok {
newIdent, err := refreshConn.Refresh(r.Context(), parseScopes(scopes), ident)
if err != nil { if err != nil {
s.logger.Errorf("failed to refresh identity: %v", err) s.logger.Errorf("failed to refresh identity: %v", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return return
} }
ident = newIdent
// Update the claims of the refresh token.
//
// UserID intentionally ignored for now.
refresh.Claims.Username = ident.Username
refresh.Claims.Email = ident.Email
refresh.Claims.EmailVerified = ident.EmailVerified
refresh.Claims.Groups = ident.Groups
refresh.ConnectorData = ident.ConnectorData
} }
idToken, expiry, err := s.newIDToken(client.ID, refresh.Claims, scopes, refresh.Nonce) claims := storage.Claims{
UserID: ident.UserID,
Username: ident.Username,
Email: ident.Email,
EmailVerified: ident.EmailVerified,
Groups: ident.Groups,
}
idToken, expiry, err := s.newIDToken(client.ID, claims, scopes, refresh.Nonce)
if err != nil { if err != nil {
s.logger.Errorf("failed to create ID token: %v", err) s.logger.Errorf("failed to create ID token: %v", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return return
} }
// Refresh tokens are claimed exactly once. Delete the current token and newToken := &internal.RefreshToken{
// create a new one. RefreshId: refresh.ID,
if err := s.storage.DeleteRefresh(code); err != nil { Token: storage.NewID(),
s.logger.Errorf("failed to delete auth code: %v", err) }
rawNewToken, err := internal.Marshal(newToken)
if err != nil {
s.logger.Errorf("failed to marshal refresh token: %v", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return return
} }
refresh.RefreshToken = storage.NewID()
if err := s.storage.CreateRefresh(refresh); err != nil { updater := func(old storage.RefreshToken) (storage.RefreshToken, error) {
s.logger.Errorf("failed to create refresh token: %v", err) if old.Token != refresh.Token {
return old, errors.New("refresh token claimed twice")
}
old.Token = newToken.Token
// Update the claims of the refresh token.
//
// UserID intentionally ignored for now.
old.Claims.Username = ident.Username
old.Claims.Email = ident.Email
old.Claims.EmailVerified = ident.EmailVerified
old.Claims.Groups = ident.Groups
old.ConnectorData = ident.ConnectorData
old.LastUsed = s.now()
return old, nil
}
if err := s.storage.UpdateRefreshToken(refresh.ID, updater); err != nil {
s.logger.Errorf("failed to update refresh token: %v", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return return
} }
s.writeAccessToken(w, idToken, refresh.RefreshToken, expiry) s.writeAccessToken(w, idToken, rawNewToken, expiry)
} }
func (s *Server) writeAccessToken(w http.ResponseWriter, idToken, refreshToken string, expiry time.Time) { func (s *Server) writeAccessToken(w http.ResponseWriter, idToken, refreshToken string, expiry time.Time) {

25
server/internal/codec.go Normal file
View file

@ -0,0 +1,25 @@
package internal
import (
"encoding/base64"
"github.com/golang/protobuf/proto"
)
// Marshal converts a protobuf message to a URL legal string.
func Marshal(message proto.Message) (string, error) {
data, err := proto.Marshal(message)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(data), nil
}
// Unmarshal decodes a protobuf message.
func Unmarshal(s string, message proto.Message) error {
data, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return err
}
return proto.Unmarshal(data, message)
}

View file

@ -0,0 +1,59 @@
// Code generated by protoc-gen-go.
// source: server/internal/types.proto
// DO NOT EDIT!
/*
Package internal is a generated protocol buffer package.
Package internal holds protobuf types used by the server
It is generated from these files:
server/internal/types.proto
It has these top-level messages:
RefreshToken
*/
package internal
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// RefreshToken is a message that holds refresh token data used by dex.
type RefreshToken struct {
RefreshId string `protobuf:"bytes,1,opt,name=refresh_id,json=refreshId" json:"refresh_id,omitempty"`
Token string `protobuf:"bytes,2,opt,name=token" json:"token,omitempty"`
}
func (m *RefreshToken) Reset() { *m = RefreshToken{} }
func (m *RefreshToken) String() string { return proto.CompactTextString(m) }
func (*RefreshToken) ProtoMessage() {}
func (*RefreshToken) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func init() {
proto.RegisterType((*RefreshToken)(nil), "internal.RefreshToken")
}
func init() { proto.RegisterFile("server/internal/types.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 112 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0x2e, 0x4e, 0x2d, 0x2a,
0x4b, 0x2d, 0xd2, 0xcf, 0xcc, 0x2b, 0x49, 0x2d, 0xca, 0x4b, 0xcc, 0xd1, 0x2f, 0xa9, 0x2c, 0x48,
0x2d, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x80, 0x89, 0x2a, 0x39, 0x73, 0xf1, 0x04,
0xa5, 0xa6, 0x15, 0xa5, 0x16, 0x67, 0x84, 0xe4, 0x67, 0xa7, 0xe6, 0x09, 0xc9, 0x72, 0x71, 0x15,
0x41, 0xf8, 0xf1, 0x99, 0x29, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x9c, 0x50, 0x11, 0xcf,
0x14, 0x21, 0x11, 0x2e, 0xd6, 0x12, 0x90, 0x3a, 0x09, 0x26, 0xb0, 0x0c, 0x84, 0x93, 0xc4, 0x06,
0x36, 0xd5, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x9b, 0xd0, 0x5a, 0x1d, 0x74, 0x00, 0x00, 0x00,
}

View file

@ -0,0 +1,10 @@
syntax = "proto3";
// Package internal holds protobuf types used by the server
package internal;
// RefreshToken is a message that holds refresh token data used by dex.
message RefreshToken {
string refresh_id = 1;
string token = 2;
}

View file

@ -237,6 +237,10 @@ func TestOAuth2CodeFlow(t *testing.T) {
if token.RefreshToken == newToken.RefreshToken { if token.RefreshToken == newToken.RefreshToken {
return fmt.Errorf("old refresh token was the same as the new token %q", token.RefreshToken) return fmt.Errorf("old refresh token was the same as the new token %q", token.RefreshToken)
} }
if _, err := config.TokenSource(ctx, token).Token(); err == nil {
return errors.New("was able to redeem the same refresh token twice")
}
return nil return nil
}, },
}, },

View file

@ -208,10 +208,14 @@ func testClientCRUD(t *testing.T, s storage.Storage) {
func testRefreshTokenCRUD(t *testing.T, s storage.Storage) { func testRefreshTokenCRUD(t *testing.T, s storage.Storage) {
id := storage.NewID() id := storage.NewID()
refresh := storage.RefreshToken{ refresh := storage.RefreshToken{
RefreshToken: id, ID: id,
Token: "bar",
Nonce: "foo",
ClientID: "client_id", ClientID: "client_id",
ConnectorID: "client_secret", ConnectorID: "client_secret",
Scopes: []string{"openid", "email", "profile"}, Scopes: []string{"openid", "email", "profile"},
CreatedAt: time.Now().UTC().Round(time.Millisecond),
LastUsed: time.Now().UTC().Round(time.Millisecond),
Claims: storage.Claims{ Claims: storage.Claims{
UserID: "1", UserID: "1",
Username: "jane", Username: "jane",
@ -238,6 +242,20 @@ func testRefreshTokenCRUD(t *testing.T, s storage.Storage) {
getAndCompare(id, refresh) getAndCompare(id, refresh)
updatedAt := time.Now().UTC().Round(time.Millisecond)
updater := func(r storage.RefreshToken) (storage.RefreshToken, error) {
r.Token = "spam"
r.LastUsed = updatedAt
return r, nil
}
if err := s.UpdateRefreshToken(id, updater); err != nil {
t.Errorf("failed to udpate refresh token: %v", err)
}
refresh.Token = "spam"
refresh.LastUsed = updatedAt
getAndCompare(id, refresh)
if err := s.DeleteRefresh(id); err != nil { if err := s.DeleteRefresh(id); err != nil {
t.Fatalf("failed to delete refresh request: %v", err) t.Fatalf("failed to delete refresh request: %v", err)
} }

View file

@ -153,23 +153,7 @@ func (cli *client) CreatePassword(p storage.Password) error {
} }
func (cli *client) CreateRefresh(r storage.RefreshToken) error { func (cli *client) CreateRefresh(r storage.RefreshToken) error {
refresh := RefreshToken{ return cli.post(resourceRefreshToken, cli.fromStorageRefreshToken(r))
TypeMeta: k8sapi.TypeMeta{
Kind: kindRefreshToken,
APIVersion: cli.apiVersion,
},
ObjectMeta: k8sapi.ObjectMeta{
Name: r.RefreshToken,
Namespace: cli.namespace,
},
ClientID: r.ClientID,
ConnectorID: r.ConnectorID,
Scopes: r.Scopes,
Nonce: r.Nonce,
Claims: fromStorageClaims(r.Claims),
ConnectorData: r.ConnectorData,
}
return cli.post(resourceRefreshToken, refresh)
} }
func (cli *client) GetAuthRequest(id string) (storage.AuthRequest, error) { func (cli *client) GetAuthRequest(id string) (storage.AuthRequest, error) {
@ -239,19 +223,16 @@ func (cli *client) GetKeys() (storage.Keys, error) {
} }
func (cli *client) GetRefresh(id string) (storage.RefreshToken, error) { func (cli *client) GetRefresh(id string) (storage.RefreshToken, error) {
var r RefreshToken r, err := cli.getRefreshToken(id)
if err := cli.get(resourceRefreshToken, id, &r); err != nil { if err != nil {
return storage.RefreshToken{}, err return storage.RefreshToken{}, err
} }
return storage.RefreshToken{ return toStorageRefreshToken(r), nil
RefreshToken: r.ObjectMeta.Name, }
ClientID: r.ClientID,
ConnectorID: r.ConnectorID, func (cli *client) getRefreshToken(id string) (r RefreshToken, err error) {
Scopes: r.Scopes, err = cli.get(resourceRefreshToken, id, &r)
Nonce: r.Nonce, return
Claims: toStorageClaims(r.Claims),
ConnectorData: r.ConnectorData,
}, nil
} }
func (cli *client) ListClients() ([]storage.Client, error) { func (cli *client) ListClients() ([]storage.Client, error) {
@ -311,6 +292,22 @@ func (cli *client) DeletePassword(email string) error {
return cli.delete(resourcePassword, p.ObjectMeta.Name) return cli.delete(resourcePassword, p.ObjectMeta.Name)
} }
func (cli *client) UpdateRefreshToken(id string, updater func(old storage.RefreshToken) (storage.RefreshToken, error)) error {
r, err := cli.getRefreshToken(id)
if err != nil {
return err
}
updated, err := updater(toStorageRefreshToken(r))
if err != nil {
return err
}
updated.ID = id
newToken := cli.fromStorageRefreshToken(updated)
newToken.ObjectMeta = r.ObjectMeta
return cli.put(resourceRefreshToken, r.ObjectMeta.Name, newToken)
}
func (cli *client) UpdateClient(id string, updater func(old storage.Client) (storage.Client, error)) error { func (cli *client) UpdateClient(id string, updater func(old storage.Client) (storage.Client, error)) error {
c, err := cli.getClient(id) c, err := cli.getClient(id)
if err != nil { if err != nil {

View file

@ -362,9 +362,14 @@ type RefreshToken struct {
k8sapi.TypeMeta `json:",inline"` k8sapi.TypeMeta `json:",inline"`
k8sapi.ObjectMeta `json:"metadata,omitempty"` k8sapi.ObjectMeta `json:"metadata,omitempty"`
CreatedAt time.Time
LastUsed time.Time
ClientID string `json:"clientID"` ClientID string `json:"clientID"`
Scopes []string `json:"scopes,omitempty"` Scopes []string `json:"scopes,omitempty"`
Token string `json:"token,omitempty"`
Nonce string `json:"nonce,omitempty"` Nonce string `json:"nonce,omitempty"`
Claims Claims `json:"claims,omitempty"` Claims Claims `json:"claims,omitempty"`
@ -379,6 +384,43 @@ type RefreshList struct {
RefreshTokens []RefreshToken `json:"items"` RefreshTokens []RefreshToken `json:"items"`
} }
func toStorageRefreshToken(r RefreshToken) storage.RefreshToken {
return storage.RefreshToken{
ID: r.ObjectMeta.Name,
Token: r.Token,
CreatedAt: r.CreatedAt,
LastUsed: r.LastUsed,
ClientID: r.ClientID,
ConnectorID: r.ConnectorID,
ConnectorData: r.ConnectorData,
Scopes: r.Scopes,
Nonce: r.Nonce,
Claims: toStorageClaims(r.Claims),
}
}
func (cli *client) fromStorageRefreshToken(r storage.RefreshToken) RefreshToken {
return RefreshToken{
TypeMeta: k8sapi.TypeMeta{
Kind: kindRefreshToken,
APIVersion: cli.apiVersion,
},
ObjectMeta: k8sapi.ObjectMeta{
Name: r.ID,
Namespace: cli.namespace,
},
Token: r.Token,
CreatedAt: r.CreatedAt,
LastUsed: r.LastUsed,
ClientID: r.ClientID,
ConnectorID: r.ConnectorID,
ConnectorData: r.ConnectorData,
Scopes: r.Scopes,
Nonce: r.Nonce,
Claims: fromStorageClaims(r.Claims),
}
}
// Keys is a mirrored struct from storage with JSON struct tags and Kubernetes // Keys is a mirrored struct from storage with JSON struct tags and Kubernetes
// type metadata. // type metadata.
type Keys struct { type Keys struct {

View file

@ -98,10 +98,10 @@ func (s *memStorage) CreateAuthCode(c storage.AuthCode) (err error) {
func (s *memStorage) CreateRefresh(r storage.RefreshToken) (err error) { func (s *memStorage) CreateRefresh(r storage.RefreshToken) (err error) {
s.tx(func() { s.tx(func() {
if _, ok := s.refreshTokens[r.RefreshToken]; ok { if _, ok := s.refreshTokens[r.ID]; ok {
err = storage.ErrAlreadyExists err = storage.ErrAlreadyExists
} else { } else {
s.refreshTokens[r.RefreshToken] = r s.refreshTokens[r.ID] = r
} }
}) })
return return
@ -324,3 +324,17 @@ func (s *memStorage) UpdatePassword(email string, updater func(p storage.Passwor
}) })
return return
} }
func (s *memStorage) UpdateRefreshToken(id string, updater func(p storage.RefreshToken) (storage.RefreshToken, error)) (err error) {
s.tx(func() {
r, ok := s.refreshTokens[id]
if !ok {
err = storage.ErrNotFound
return
}
if r, err = updater(r); err == nil {
s.refreshTokens[id] = r
}
})
return
}

View file

@ -244,14 +244,16 @@ func (c *conn) CreateRefresh(r storage.RefreshToken) error {
id, client_id, scopes, nonce, id, client_id, scopes, nonce,
claims_user_id, claims_username, claims_email, claims_email_verified, claims_user_id, claims_username, claims_email, claims_email_verified,
claims_groups, claims_groups,
connector_id, connector_data connector_id, connector_data,
token, created_at, last_used
) )
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11); values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14);
`, `,
r.RefreshToken, r.ClientID, encoder(r.Scopes), r.Nonce, r.ID, r.ClientID, encoder(r.Scopes), r.Nonce,
r.Claims.UserID, r.Claims.Username, r.Claims.Email, r.Claims.EmailVerified, r.Claims.UserID, r.Claims.Username, r.Claims.Email, r.Claims.EmailVerified,
encoder(r.Claims.Groups), encoder(r.Claims.Groups),
r.ConnectorID, r.ConnectorData, r.ConnectorID, r.ConnectorData,
r.Token, r.CreatedAt, r.LastUsed,
) )
if err != nil { if err != nil {
return fmt.Errorf("insert refresh_token: %v", err) return fmt.Errorf("insert refresh_token: %v", err)
@ -259,13 +261,57 @@ func (c *conn) CreateRefresh(r storage.RefreshToken) error {
return nil return nil
} }
func (c *conn) UpdateRefreshToken(id string, updater func(old storage.RefreshToken) (storage.RefreshToken, error)) error {
return c.ExecTx(func(tx *trans) error {
r, err := getRefresh(tx, id)
if err != nil {
return err
}
if r, err = updater(r); err != nil {
return err
}
_, err = tx.Exec(`
update refresh_token
set
client_id = $1,
scopes = $2,
nonce = $3,
claims_user_id = $4,
claims_username = $5,
claims_email = $6,
claims_email_verified = $7,
claims_groups = $8,
connector_id = $9,
connector_data = $10,
token = $11,
created_at = $12,
last_used = $13
`,
r.ClientID, encoder(r.Scopes), r.Nonce,
r.Claims.UserID, r.Claims.Username, r.Claims.Email, r.Claims.EmailVerified,
encoder(r.Claims.Groups),
r.ConnectorID, r.ConnectorData,
r.Token, r.CreatedAt, r.LastUsed,
)
if err != nil {
return fmt.Errorf("update refresh token: %v", err)
}
return nil
})
}
func (c *conn) GetRefresh(id string) (storage.RefreshToken, error) { func (c *conn) GetRefresh(id string) (storage.RefreshToken, error) {
return scanRefresh(c.QueryRow(` return getRefresh(c, id)
}
func getRefresh(q querier, id string) (storage.RefreshToken, error) {
return scanRefresh(q.QueryRow(`
select select
id, client_id, scopes, nonce, id, client_id, scopes, nonce,
claims_user_id, claims_username, claims_email, claims_email_verified, claims_user_id, claims_username, claims_email, claims_email_verified,
claims_groups, claims_groups,
connector_id, connector_data connector_id, connector_data,
token, created_at, last_used
from refresh_token where id = $1; from refresh_token where id = $1;
`, id)) `, id))
} }
@ -276,7 +322,8 @@ func (c *conn) ListRefreshTokens() ([]storage.RefreshToken, error) {
id, client_id, scopes, nonce, id, client_id, scopes, nonce,
claims_user_id, claims_username, claims_email, claims_email_verified, claims_user_id, claims_username, claims_email, claims_email_verified,
claims_groups, claims_groups,
connector_id, connector_data connector_id, connector_data,
token, created_at, last_used
from refresh_token; from refresh_token;
`) `)
if err != nil { if err != nil {
@ -298,10 +345,11 @@ func (c *conn) ListRefreshTokens() ([]storage.RefreshToken, error) {
func scanRefresh(s scanner) (r storage.RefreshToken, err error) { func scanRefresh(s scanner) (r storage.RefreshToken, err error) {
err = s.Scan( err = s.Scan(
&r.RefreshToken, &r.ClientID, decoder(&r.Scopes), &r.Nonce, &r.ID, &r.ClientID, decoder(&r.Scopes), &r.Nonce,
&r.Claims.UserID, &r.Claims.Username, &r.Claims.Email, &r.Claims.EmailVerified, &r.Claims.UserID, &r.Claims.Username, &r.Claims.Email, &r.Claims.EmailVerified,
decoder(&r.Claims.Groups), decoder(&r.Claims.Groups),
&r.ConnectorID, &r.ConnectorData, &r.ConnectorID, &r.ConnectorData,
&r.Token, &r.CreatedAt, &r.LastUsed,
) )
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {

View file

@ -155,4 +155,14 @@ var migrations = []migration{
); );
`, `,
}, },
{
stmt: `
alter table refresh_token
add column token text not null default '';
alter table refresh_token
add column created_at timestamptz not null default '0001-01-01 00:00:00 UTC';
alter table refresh_token
add column last_used timestamptz not null default '0001-01-01 00:00:00 UTC';
`,
},
} }

View file

@ -94,6 +94,7 @@ type Storage interface {
UpdateClient(id string, updater func(old Client) (Client, error)) error UpdateClient(id string, updater func(old Client) (Client, error)) error
UpdateKeys(updater func(old Keys) (Keys, error)) error UpdateKeys(updater func(old Keys) (Keys, error)) error
UpdateAuthRequest(id string, updater func(a AuthRequest) (AuthRequest, error)) error UpdateAuthRequest(id string, updater func(a AuthRequest) (AuthRequest, error)) error
UpdateRefreshToken(id string, updater func(r RefreshToken) (RefreshToken, error)) error
UpdatePassword(email string, updater func(p Password) (Password, error)) error UpdatePassword(email string, updater func(p Password) (Password, error)) error
// GarbageCollect deletes all expired AuthCodes and AuthRequests. // GarbageCollect deletes all expired AuthCodes and AuthRequests.
@ -216,8 +217,15 @@ type AuthCode struct {
// RefreshToken is an OAuth2 refresh token which allows a client to request new // RefreshToken is an OAuth2 refresh token which allows a client to request new
// tokens on the end user's behalf. // tokens on the end user's behalf.
type RefreshToken struct { type RefreshToken struct {
// The actual refresh token. ID string
RefreshToken string
// A single token that's rotated every time the refresh token is refreshed.
//
// May be empty.
Token string
CreatedAt time.Time
LastUsed time.Time
// Client this refresh token is valid for. // Client this refresh token is valid for.
ClientID string ClientID string