Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions services/auth-guest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ links feature and start the service.
PIN; redeeming a token or verifying a PIN sets a session cookie. `renew` and
`verify/pin` are rate-limited.
- Stores only hashes of the token secret, of the PIN and of the nonce, and
deletes the stored record when the share is removed or expires.
deletes the stored record when the share is removed or expires.

## Guest links flow

Expand Down Expand Up @@ -209,9 +209,11 @@ sequenceDiagram
publishes `GuestTokenRenewed` without a PIN; no nonce cookie is set. Each
path is limited per share by its own quota (cooldown plus daily cap), and an
exceeded limit fails with `429`. Without a valid session nothing happens
while the newest link is neither used nor expired. Both paths require an
existing record; otherwise the request fails with `tokenNotFound`. If
publishing fails, the previous record is restored.
while the newest link is neither used nor expired. The response body's
`withPin` reports whether the mail carries a PIN, which tells the
session-authorized path apart from the silent renewal. Both paths require an
existing record; otherwise the request fails with `tokenNotFound`.
If publishing fails, the previous record is restored.
4. **Verify PIN** — the guest posts the PIN, the `permissionId` and the nonce
cookie to
`POST /graph/v1beta1/extensions/org.libregraph/guestLinks/verify/pin`.
Expand Down Expand Up @@ -245,10 +247,12 @@ Rate-limited responses (`429`) also carry a `retryAfter` field (seconds) and a
- **Authorization:** the session cookie, when it carries a valid, unexpired
session token for the given share; without it the renewal is silent (no PIN,
no nonce cookie)
- **Success:** `200`, no session cookie. With a valid session a new link and
PIN are sent by mail and the nonce cookie is set. Without one a new link is
sent by mail, unless the newest link can still be redeemed, in which case
nothing happens; the response is the same either way.
- **Success:** `200`, no session cookie, `{ "withPin": <bool> }`. `withPin` is
`true` only when a valid session authorized the renewal and a new link and PIN
were sent by mail (the nonce cookie is set as well). In every other successful
case — silent rotation without a PIN, or nothing sent because the newest link
can still be redeemed — it is `false`, which keeps those cases
indistinguishable to callers without a session.
- **Errors:** `400 invalidRequest`, `404 tokenNotFound`, `404 shareNotFound`,
`409 conflict`, `410 shareExpired`, `429 tooManyRequests` (with
`Retry-After`), `503 serviceUnavailable`, `500 internalError`
Expand Down
13 changes: 12 additions & 1 deletion services/auth-guest/pkg/server/http/renew.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,16 @@ type RenewRequest struct {
PermissionID string `json:"permissionId"`
}

// RenewResponse is the response body of the renew endpoint. WithPin reports
// whether the renewal email contains a PIN, which is true exactly when the
// request was authorized by a valid session.
type RenewResponse struct {
WithPin bool `json:"withPin"`
}

// RenewHandler generates a new guest link token and PIN for the share
// identified by the permission id, authorized by the session cookie.
// identified by the permission id, authorized by the session cookie. The
// response body reports whether the renewal email contains a PIN.
func RenewHandler(log log.Logger, s authguest.AuthGuest, cfg *config.Config) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var req RenewRequest
Expand Down Expand Up @@ -51,6 +59,9 @@ func RenewHandler(log log.Logger, s authguest.AuthGuest, cfg *config.Config) fun
setNonceCookie(w, cfg, req.PermissionID, nonce)
}

// A non-empty nonce means the PIN path ran, so the mail carries a PIN.
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(RenewResponse{WithPin: nonce != ""})
}
}
15 changes: 14 additions & 1 deletion services/auth-guest/pkg/server/http/renew_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,13 @@ func TestRenewHandlerWithSessionCookie(t *testing.T) {
newRenewHandler(t, svcMock)(rr, req)

assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
svcMock.AssertCalled(t, "Renew", mock.Anything, "share-1", "session-token")

var resp RenewResponse
require.NoError(t, json.NewDecoder(rr.Body).Decode(&resp))
assert.True(t, resp.WithPin)

wantName := nonceCookieName(testNonceCookieName, "share-1")
var cookie *http.Cookie
for _, c := range rr.Result().Cookies() {
Expand All @@ -81,7 +86,7 @@ func TestRenewHandlerWithSessionCookie(t *testing.T) {

func TestRenewHandlerWithoutSessionCookie(t *testing.T) {
svcMock := mocks.NewAuthGuest(t)
svcMock.On("Renew", mock.Anything, "share-1", "").Return("nonce-value", nil)
svcMock.On("Renew", mock.Anything, "share-1", "").Return("", nil)

body, err := json.Marshal(RenewRequest{PermissionID: "share-1"})
require.NoError(t, err)
Expand All @@ -91,6 +96,10 @@ func TestRenewHandlerWithoutSessionCookie(t *testing.T) {

assert.Equal(t, http.StatusOK, rr.Code)
svcMock.AssertCalled(t, "Renew", mock.Anything, "share-1", "")

var resp RenewResponse
require.NoError(t, json.NewDecoder(rr.Body).Decode(&resp))
assert.False(t, resp.WithPin)
}

func TestRenewHandlerSkipsNonceCookieWhenEmpty(t *testing.T) {
Expand All @@ -113,6 +122,10 @@ func TestRenewHandlerSkipsNonceCookieWhenEmpty(t *testing.T) {

assert.Equal(t, http.StatusOK, rr.Code)
assert.Empty(t, rr.Result().Cookies())

var resp RenewResponse
require.NoError(t, json.NewDecoder(rr.Body).Decode(&resp))
assert.False(t, resp.WithPin)
}

func TestRenewHandlerNonceCookiePerShare(t *testing.T) {
Expand Down