Skip to content

Commit

Permalink
feat: refresh tokens, refresh grant & rotation (#1294)
Browse files Browse the repository at this point in the history
  • Loading branch information
davenewza authored Nov 13, 2023
1 parent 569b8ba commit 7fc95de
Show file tree
Hide file tree
Showing 17 changed files with 668 additions and 292 deletions.
4 changes: 0 additions & 4 deletions cmd/program/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,6 @@ func RunMigrations(schema *proto.Schema, database db.Database) tea.Cmd {
Changes: m.Changes,
}

if !m.HasModelFieldChanges() {
return msg
}

err = m.Apply(context.Background())
if err != nil {
msg.Err = &ApplyMigrationsError{
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/dchest/uniuri v1.2.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
Expand Down
2 changes: 1 addition & 1 deletion migrations/columns.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_catalog.pg_index i on i.indexrelid = a.attrelid
WHERE
n.nspname = 'public'
AND c.relname not in ('keel_schema', 'pg_stat_statements_info', 'pg_stat_statements')
AND c.relname not in ('keel_schema', 'keel_refresh_token', 'pg_stat_statements_info', 'pg_stat_statements')
AND a.attnum > 0
AND NOT a.attisdropped
AND i.indexrelid is null; -- no indexes
5 changes: 4 additions & 1 deletion migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (m *Migrations) Apply(ctx context.Context) error {
sql.WriteString(setUpdatedAt)
sql.WriteString("\n")

sql.WriteString("CREATE TABLE IF NOT EXISTS keel_schema ( schema TEXT NOT NULL );\n")
sql.WriteString("CREATE TABLE IF NOT EXISTS keel_schema (schema TEXT NOT NULL);\n")
sql.WriteString("DELETE FROM keel_schema;\n")

b, err := protojson.Marshal(m.Schema)
Expand All @@ -113,6 +113,9 @@ func (m *Migrations) Apply(ctx context.Context) error {
sql.WriteString(fmt.Sprintf("INSERT INTO keel_schema (schema) VALUES (%s);", escapedJSON))
sql.WriteString("\n")

sql.WriteString("CREATE TABLE IF NOT EXISTS keel_refresh_token (token TEXT NOT NULL PRIMARY KEY, identity_id TEXT NOT NULL, created_at TIMESTAMP, expires_at TIMESTAMP);\n")
sql.WriteString("\n")

sql.WriteString(fmt.Sprintf("SELECT set_trace_id('%s');\n", span.SpanContext().TraceID().String()))

sql.WriteString(m.SQL)
Expand Down
16 changes: 16 additions & 0 deletions runtime/apis/authapi/revoke_endpoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package authapi

import (
"net/http"

"github.com/teamkeel/keel/proto"
"github.com/teamkeel/keel/runtime/common"
)

func RevokeHandler(schema *proto.Schema) common.HandlerFunc {
return func(r *http.Request) common.Response {
return common.Response{
Status: http.StatusNotImplemented,
}
}
}
64 changes: 60 additions & 4 deletions runtime/apis/authapi/token_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ const (
ArgSubjectToken = "subject_token"
ArgSubjectTokenType = "subject_token_type"
ArgRequestedTokeType = "requested_token_type"
ArgRefreshToken = "refresh_token"
)

const (
TokenType = "bearer"
)

// https://openid.net/specs/openid-connect-standard-1_0-21_orig.html#AccessTokenResponse
Expand Down Expand Up @@ -90,7 +95,50 @@ func TokenEndpointHandler(schema *proto.Schema) common.HandlerFunc {

switch grantType {
case GrantTypeRefreshToken:
return common.Response{Status: http.StatusNotImplemented}
if !r.Form.Has(ArgRefreshToken) {
return common.NewJsonResponse(http.StatusBadRequest, &TokenErrorResponse{
Error: TokenEndpointInvalidRequest,
ErrorDescription: "the refresh token must be provided in the refresh_token field",
}, nil)
}

refreshTokenRaw := r.Form.Get(ArgRefreshToken)

if refreshTokenRaw == "" {
return common.NewJsonResponse(http.StatusBadRequest, &TokenErrorResponse{
Error: TokenEndpointInvalidRequest,
ErrorDescription: "the refresh token in the refresh_token field cannot be an empty string",
}, nil)
}

isValid, newRefreshToken, identityId, err := oauth.RotateRefreshToken(ctx, refreshTokenRaw)
if err != nil {
span.RecordError(err)
return common.NewJsonResponse(http.StatusInternalServerError, nil, nil)
}

if !isValid {
return common.NewJsonResponse(http.StatusUnauthorized, &TokenErrorResponse{
Error: TokenEndpointInvalidClient,
ErrorDescription: "possible causes may be that the refresh token has been revoked or has expired",
}, nil)
}

// Generate an access token for this identity.
accessTokenRaw, expiresIn, err := oauth.GenerateAccessToken(ctx, identityId)
if err != nil {
span.RecordError(err)
return common.NewJsonResponse(http.StatusInternalServerError, nil, nil)
}

response := &TokenResponse{
AccessToken: accessTokenRaw,
TokenType: TokenType,
ExpiresIn: int(expiresIn.Seconds()),
RefreshToken: newRefreshToken,
}

return common.NewJsonResponse(http.StatusOK, response, nil)
case GrantTypeTokenExchange:
if !r.Form.Has(ArgSubjectToken) {
return common.NewJsonResponse(http.StatusBadRequest, &TokenErrorResponse{
Expand Down Expand Up @@ -176,10 +224,18 @@ func TokenEndpointHandler(schema *proto.Schema) common.HandlerFunc {
return common.NewJsonResponse(http.StatusInternalServerError, nil, nil)
}

// Generate a refresh token.
refreshTokenRaw, err := oauth.NewRefreshToken(ctx, identity.Id)
if err != nil {
span.RecordError(err)
return common.NewJsonResponse(http.StatusInternalServerError, nil, nil)
}

response := &TokenResponse{
AccessToken: accessTokenRaw,
TokenType: "bearer",
ExpiresIn: int(expiresIn.Seconds()),
AccessToken: accessTokenRaw,
TokenType: TokenType,
ExpiresIn: int(expiresIn.Seconds()),
RefreshToken: refreshTokenRaw,
}

return common.NewJsonResponse(http.StatusOK, response, nil)
Expand Down
Loading

0 comments on commit 7fc95de

Please sign in to comment.