mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-15 21:48:13 +03:00
fix: re-check api permissions on access token refresh
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
@@ -52,6 +53,13 @@ func (h *tokenHandler) token(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = h.validateRefreshAPIGrant(ctx, client, accessRequest)
|
||||||
|
if err != nil {
|
||||||
|
slog.WarnContext(ctx, "Rejected refresh token request: API grant is no longer allowed for the user subject", "error", err.Error())
|
||||||
|
h.provider.WriteAccessError(ctx, c.Writer, accessRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// The client credentials grant has no authorize step so the RFC 8707 resource is resolved here to stamp the API audience and limit the granted scope to what the client is allowed for that API
|
// The client credentials grant has no authorize step so the RFC 8707 resource is resolved here to stamp the API audience and limit the granted scope to what the client is allowed for that API
|
||||||
// It resolves against the client-subject grants: a permission delegated by users does not let the client act as itself
|
// It resolves against the client-subject grants: a permission delegated by users does not let the client act as itself
|
||||||
// The other grants had their audience and scope resolved at authorize or device time and restored from storage, so they must be left untouched
|
// The other grants had their audience and scope resolved at authorize or device time and restored from storage, so they must be left untouched
|
||||||
@@ -103,3 +111,36 @@ func (h *tokenHandler) token(c *gin.Context) {
|
|||||||
|
|
||||||
h.provider.WriteAccessResponse(ctx, c.Writer, accessRequest, response)
|
h.provider.WriteAccessResponse(ctx, c.Writer, accessRequest, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *tokenHandler) validateRefreshAPIGrant(ctx context.Context, client Client, accessRequest fosite.AccessRequester) error {
|
||||||
|
if !accessRequest.GetGrantTypes().Has(string(fosite.GrantTypeRefreshToken)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
issuer := ""
|
||||||
|
if h.claimsService != nil {
|
||||||
|
issuer = h.claimsService.baseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
resource, err := refreshGrantResource(client.GetID(), issuer, accessRequest.GetGrantedAudience())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = resolveResource(ctx, h.apiAccess, client.GetID(), resource, accessRequest.GetGrantedScopes(), SubjectTypeUser)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshGrantResource(clientID, issuer string, grantedAudience fosite.Arguments) (string, error) {
|
||||||
|
resource := ""
|
||||||
|
for _, audience := range grantedAudience {
|
||||||
|
if audience == "" || audience == clientID || audience == issuer {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resource != "" && resource != audience {
|
||||||
|
return "", fosite.ErrInvalidTarget.WithHint("Refresh-token grants may only target one API resource.")
|
||||||
|
}
|
||||||
|
resource = audience
|
||||||
|
}
|
||||||
|
return resource, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -551,7 +551,7 @@ func TestTokenHandlerRefreshGrantPreservesAudienceAndScope(t *testing.T) {
|
|||||||
Secret: []byte(secret),
|
Secret: []byte(secret),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
handler := newTokenHandler(provider, newClaimsService(db, nil, baseURL, nil), nil)
|
handler := newTokenHandler(provider, newClaimsService(db, nil, baseURL, nil), apiAccess)
|
||||||
|
|
||||||
form := url.Values{
|
form := url.Values{
|
||||||
"grant_type": {"refresh_token"},
|
"grant_type": {"refresh_token"},
|
||||||
@@ -637,4 +637,47 @@ func TestTokenHandlerRefreshGrantPreservesAudienceAndScope(t *testing.T) {
|
|||||||
require.Empty(t, body["access_token"])
|
require.Empty(t, body["access_token"])
|
||||||
require.Equal(t, "invalid_scope", body["error"])
|
require.Equal(t, "invalid_scope", body["error"])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("revoking only the user-delegated API grant makes the next refresh fail", func(t *testing.T) {
|
||||||
|
db := testutils.NewDatabaseForTest(t)
|
||||||
|
const clientID, userID = "client-user-revoked", "user-user-revoked"
|
||||||
|
seedUserAndClient(t, db, clientID, userID)
|
||||||
|
|
||||||
|
clientOnlyAPI := fakeAPIAccess{allowed: map[string]map[SubjectType][]string{
|
||||||
|
apiResource: {
|
||||||
|
SubjectTypeClient: {"read:orders"},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
token := mintRefreshToken(t, db, clientID, userID,
|
||||||
|
fosite.Arguments{"openid", "read:orders"},
|
||||||
|
fosite.Arguments{apiResource},
|
||||||
|
)
|
||||||
|
body := doRefresh(t, db, clientOnlyAPI, clientID, token, nil)
|
||||||
|
require.Empty(t, body["access_token"])
|
||||||
|
require.Equal(t, "access_denied", body["error"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("refresh rechecks the granted scope against the granted API audience", func(t *testing.T) {
|
||||||
|
db := testutils.NewDatabaseForTest(t)
|
||||||
|
const clientID, userID = "client-split-grant", "user-split-grant"
|
||||||
|
seedUserAndClient(t, db, clientID, userID)
|
||||||
|
|
||||||
|
splitGrantAPI := fakeAPIAccess{allowed: map[string]map[SubjectType][]string{
|
||||||
|
apiResource: {
|
||||||
|
SubjectTypeUser: {"write:orders"},
|
||||||
|
},
|
||||||
|
"https://api.inventory.example.com": {
|
||||||
|
SubjectTypeUser: {"read:orders"},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
token := mintRefreshToken(t, db, clientID, userID,
|
||||||
|
fosite.Arguments{"openid", "read:orders"},
|
||||||
|
fosite.Arguments{apiResource},
|
||||||
|
)
|
||||||
|
body := doRefresh(t, db, splitGrantAPI, clientID, token, nil)
|
||||||
|
require.Empty(t, body["access_token"])
|
||||||
|
require.Equal(t, "invalid_scope", body["error"])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user