diff --git a/web/api/refresh_token.go b/web/api/refresh_token.go index ebde6492..45ef1d58 100644 --- a/web/api/refresh_token.go +++ b/web/api/refresh_token.go @@ -3,13 +3,14 @@ package api import ( "encoding/json" "net/http" + "strings" l "github.com/madappgang/identifo/v2/localization" "github.com/madappgang/identifo/v2/model" "github.com/madappgang/identifo/v2/web/middleware" ) -// RefreshTokens issues new access and, if requsted, refresh token for provided refresh token. +// RefreshTokens issues new access and, if requested, refresh token for provided refresh token. // After new tokens are issued, the old refresh token gets invalidated (via blacklisting). func (ar *Router) RefreshTokens() http.HandlerFunc { type requestData struct { @@ -22,6 +23,8 @@ func (ar *Router) RefreshTokens() http.HandlerFunc { } return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + locale := r.Header.Get("Accept-Language") rd := requestData{} @@ -30,14 +33,14 @@ func (ar *Router) RefreshTokens() http.HandlerFunc { rd = requestData{Scopes: []string{}} } - app := middleware.AppFromContext(r.Context()) + app := middleware.AppFromContext(ctx) if len(app.ID) == 0 { ar.Error(w, locale, http.StatusBadRequest, l.ErrorAPIAPPNoAPPInContext) return } // Get refresh token from context. - oldRefreshToken := tokenFromContext(r.Context()) + oldRefreshToken := tokenFromContext(ctx) if err := oldRefreshToken.Validate(); err != nil { ar.Error(w, locale, http.StatusUnauthorized, l.ErrorTokenInvalidError, err) @@ -84,12 +87,19 @@ func (ar *Router) RefreshTokens() http.HandlerFunc { RefreshToken: newRefreshTokenString, } + resultScopes := strings.Split(accessToken.Scopes(), " ") + journal(oldRefreshToken.Subject(), app.ID, "refresh_token", resultScopes) + ar.ServeJSON(w, locale, http.StatusOK, result) } } -func (ar *Router) issueNewRefreshToken(oldRefreshTokenString string, scopes []string, app model.AppData) (string, error) { - if !contains(scopes, model.OfflineScope) { // Don't issue new refresh token if not requested. +func (ar *Router) issueNewRefreshToken( + oldRefreshTokenString string, + requestedScopes []string, + app model.AppData, +) (string, error) { + if !contains(requestedScopes, model.OfflineScope) { // Don't issue new refresh token if not requested. return "", nil } @@ -103,6 +113,8 @@ func (ar *Router) issueNewRefreshToken(oldRefreshTokenString string, scopes []st return "", err } + scopes := model.AllowedScopes(requestedScopes, user.Scopes, app.Offline) + refreshToken, err := ar.server.Services().Token.NewRefreshToken(user, scopes, app) if err != nil { return "", err diff --git a/web/api/refresh_token_test.go b/web/api/refresh_token_test.go new file mode 100644 index 00000000..48023740 --- /dev/null +++ b/web/api/refresh_token_test.go @@ -0,0 +1,67 @@ +package api_test + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/madappgang/identifo/v2/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRefreshTokens(t *testing.T) { + ctx := testContext(testApp) + + reqBody := `{"scopes":["offline", "chat", "super_admin"]}` + + user := model.User{ + ID: "test_user", + Scopes: []string{"chat"}, + Active: true, + Email: "som@example.com", + AccessRole: "user", + } + + user, err := testServer.Storages().User.AddUserWithPassword(user, "qwerty", "user", false) + require.NoError(t, err) + + tokenService := testServer.Services().Token + + refreshToken, err := tokenService.NewRefreshToken( + user, + []string{"offline", "chat", "super_admin"}, + testApp) + require.NoError(t, err) + + rts, err := tokenService.String(refreshToken) + require.NoError(t, err) + + refreshToken, err = tokenService.Parse(rts) + require.NoError(t, err) + + ctx = context.WithValue(ctx, model.TokenContextKey, refreshToken) + ctx = context.WithValue(ctx, model.TokenRawContextKey, []byte(rts)) + + req := httptest.NewRequest(http.MethodPost, "/auth/token", strings.NewReader(reqBody)) + req = req.WithContext(ctx) + + rw := httptest.NewRecorder() + + h := testRouter.RefreshTokens() + h(rw, req) + + require.Equal(t, http.StatusOK, rw.Code, rw.Body.String()) + + c := claimsFromResponse(t, rw.Body.Bytes()) + assert.Equal(t, user.ID, c["sub"]) + assert.Equal(t, "test_app", c["aud"]) + assert.Equal(t, "chat offline", c["scopes"]) + + c = refreshClaimsFromResponse(t, rw.Body.Bytes()) + assert.Equal(t, user.ID, c["sub"]) + assert.Equal(t, "test_app", c["aud"]) + assert.Equal(t, "chat offline", c["scopes"]) +}