feat: add CanRefreshTokenInfo to support non-JWT refresh tokens (#244)
* Add an additional, optional, op.Storage interface so that refresh tokens that are not JWTs do not cause failures when they randomly, sometimes, decrypt without error ```go // CanRefreshTokenInfo is an optional additional interface that Storage can support. // Supporting CanRefreshTokenInfo is required to be able to revoke a refresh token that // does not happen to also be a JWTs work properly. type CanRefreshTokenInfo interface { // GetRefreshTokenInfo must return oidc.ErrInvalidRefreshToken when presented // with a token that is not a refresh token. GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) } ``` * add comment suggested in code review * review feedback: return an error defined in op rather than adding a new error to oidc * move ErrInvalidRefresToken to op/storage.go
This commit is contained in:
parent
fa222c5efb
commit
cdf2af6c2c
3 changed files with 37 additions and 4 deletions
|
@ -3,4 +3,5 @@
|
||||||
|
|
||||||
- Add `rp/RelyingParty.GetRevokeEndpoint`
|
- Add `rp/RelyingParty.GetRevokeEndpoint`
|
||||||
- Rename `op/OpStorage.GetKeyByIDAndUserID` to `op/OpStorage.GetKeyByIDAndClientID`
|
- Rename `op/OpStorage.GetKeyByIDAndUserID` to `op/OpStorage.GetKeyByIDAndClientID`
|
||||||
|
- Add `CanRefreshTokenInfo` (`GetRefreshTokenInfo()`) to `op.Storage`
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
@ -50,6 +51,17 @@ type AuthStorage interface {
|
||||||
GetKeySet(context.Context) (*jose.JSONWebKeySet, error)
|
GetKeySet(context.Context) (*jose.JSONWebKeySet, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CanRefreshTokenInfo is an optional additional interface that Storage can support.
|
||||||
|
// Supporting CanRefreshTokenInfo is required to be able to (revoke) a refresh token that
|
||||||
|
// is neither an encrypted string of <tokenID>:<userID> nor a JWT.
|
||||||
|
type CanRefreshTokenInfo interface {
|
||||||
|
// GetRefreshTokenInfo must return ErrInvalidRefreshToken when presented
|
||||||
|
// with a token that is not a refresh token.
|
||||||
|
GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrInvalidRefreshToken = errors.New("invalid_refresh_token")
|
||||||
|
|
||||||
type ClientCredentialsStorage interface {
|
type ClientCredentialsStorage interface {
|
||||||
ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error)
|
ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -31,14 +32,33 @@ func revocationHandler(revoker Revoker) func(http.ResponseWriter, *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Revoke(w http.ResponseWriter, r *http.Request, revoker Revoker) {
|
func Revoke(w http.ResponseWriter, r *http.Request, revoker Revoker) {
|
||||||
token, _, clientID, err := ParseTokenRevocationRequest(r, revoker)
|
token, tokenTypeHint, clientID, err := ParseTokenRevocationRequest(r, revoker)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RevocationRequestError(w, r, err)
|
RevocationRequestError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tokenID, subject, ok := getTokenIDAndSubjectForRevocation(r.Context(), revoker, token)
|
var subject string
|
||||||
|
doDecrypt := true
|
||||||
|
if canRefreshInfo, ok := revoker.Storage().(CanRefreshTokenInfo); ok && tokenTypeHint != "access_token" {
|
||||||
|
userID, tokenID, err := canRefreshInfo.GetRefreshTokenInfo(r.Context(), clientID, token)
|
||||||
|
if err != nil {
|
||||||
|
// An invalid refresh token means that we'll try other things (leaving doDecrypt==true)
|
||||||
|
if !errors.Is(err, ErrInvalidRefreshToken) {
|
||||||
|
RevocationRequestError(w, r, oidc.ErrServerError().WithParent(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
token = tokenID
|
||||||
|
subject = userID
|
||||||
|
doDecrypt = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if doDecrypt {
|
||||||
|
tokenID, userID, ok := getTokenIDAndSubjectForRevocation(r.Context(), revoker, token)
|
||||||
if ok {
|
if ok {
|
||||||
token = tokenID
|
token = tokenID
|
||||||
|
subject = userID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := revoker.Storage().RevokeToken(r.Context(), token, subject, clientID); err != nil {
|
if err := revoker.Storage().RevokeToken(r.Context(), token, subject, clientID); err != nil {
|
||||||
RevocationRequestError(w, r, err)
|
RevocationRequestError(w, r, err)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue