Fix broken login on omsorgWeb: restore refreshToken in auth responses
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 14s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Successful in 5s
Docker-Images bauen und veröffentlichen / build (, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 17s
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 14s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Successful in 5s
Docker-Images bauen und veröffentlichen / build (, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 17s
The cookie-only refresh-token migration earlier this session broke both mitarbeiter-app and mitarbeiter-app-legacy: they're server-to-server PHP clients (cURL/Guzzle calling omsorgCore directly) with no browser cookie jar, so dropping refreshToken from the login/refresh response body left them with nothing to store - login appeared to succeed, redirected to the dashboard, but the very next page's session check failed silently (mitarbeiter-app's _ensure_fresh_token() bails out whenever $_SESSION['omsorgcore_refresh_token'] is empty), bouncing the user back to the login form every time. Fix: dual-mode refresh token transport instead of cookie-only. - LoginResponse includes refreshToken again (restores the pre-migration contract PHP already expected) alongside the HttpOnly cookie. - AuthController.Refresh/Logout accept an optional body-carried RefreshRequest/LogoutRequest as a fallback: cookie is checked first (browser/omsorgapp), body second (server-to-server clients). - omsorgapp keeps ignoring the body's refreshToken and relies solely on the cookie (XSS-safe) - only its authApi.js needed a small update since the regenerated client now requires an explicit (empty) parameter object for refresh/logout. - Regenerated omsorgcore-client-ts; api-client-php's lib/ was already consistent (never regenerated during the original migration, so it still expected refreshToken all along - only the backend had stopped providing it). Verified end-to-end against a live instance: PHP login+refresh via omsorgcore_login()/omsorgcore_refresh(), and the browser cookie-only flow via curl with Origin/credentials headers - both work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
dca0349e8c
commit
09dce2ab98
@@ -57,6 +57,7 @@ src/models/ForgotPasswordVerifyRequest.ts
|
||||
src/models/ForgotPasswordVerifyResponse.ts
|
||||
src/models/LoginRequest.ts
|
||||
src/models/LoginResponse.ts
|
||||
src/models/LogoutRequest.ts
|
||||
src/models/MeResponse.ts
|
||||
src/models/ModuleType.ts
|
||||
src/models/OrderResponse.ts
|
||||
@@ -67,6 +68,7 @@ src/models/PermissionAction.ts
|
||||
src/models/PermissionDto.ts
|
||||
src/models/PermissionEffect.ts
|
||||
src/models/PermissionScope.ts
|
||||
src/models/RefreshRequest.ts
|
||||
src/models/ResetUserPasswordRequest.ts
|
||||
src/models/RolePermissionsResponse.ts
|
||||
src/models/RoleResponse.ts
|
||||
|
||||
@@ -23,8 +23,10 @@ import type {
|
||||
ForgotPasswordVerifyResponse,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
LogoutRequest,
|
||||
MeResponse,
|
||||
PasswordPolicyResponse,
|
||||
RefreshRequest,
|
||||
} from '../models/index';
|
||||
import {
|
||||
ChangePasswordRequestFromJSON,
|
||||
@@ -43,10 +45,14 @@ import {
|
||||
LoginRequestToJSON,
|
||||
LoginResponseFromJSON,
|
||||
LoginResponseToJSON,
|
||||
LogoutRequestFromJSON,
|
||||
LogoutRequestToJSON,
|
||||
MeResponseFromJSON,
|
||||
MeResponseToJSON,
|
||||
PasswordPolicyResponseFromJSON,
|
||||
PasswordPolicyResponseToJSON,
|
||||
RefreshRequestFromJSON,
|
||||
RefreshRequestToJSON,
|
||||
} from '../models/index';
|
||||
|
||||
export interface ApiAuthChangePasswordPostRequest {
|
||||
@@ -69,6 +75,14 @@ export interface ApiAuthLoginPostRequest {
|
||||
loginRequest?: LoginRequest;
|
||||
}
|
||||
|
||||
export interface ApiAuthLogoutPostRequest {
|
||||
logoutRequest?: LogoutRequest;
|
||||
}
|
||||
|
||||
export interface ApiAuthRefreshPostRequest {
|
||||
refreshRequest?: RefreshRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -264,11 +278,13 @@ export class AuthApi extends runtime.BaseAPI {
|
||||
|
||||
/**
|
||||
*/
|
||||
async apiAuthLogoutPostRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> {
|
||||
async apiAuthLogoutPostRaw(requestParameters: ApiAuthLogoutPostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> {
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
headerParameters['Content-Type'] = 'application/json';
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("Bearer", []);
|
||||
@@ -285,6 +301,7 @@ export class AuthApi extends runtime.BaseAPI {
|
||||
method: 'POST',
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
body: LogoutRequestToJSON(requestParameters['logoutRequest']),
|
||||
}, initOverrides);
|
||||
|
||||
return new runtime.VoidApiResponse(response);
|
||||
@@ -292,8 +309,8 @@ export class AuthApi extends runtime.BaseAPI {
|
||||
|
||||
/**
|
||||
*/
|
||||
async apiAuthLogoutPost(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> {
|
||||
await this.apiAuthLogoutPostRaw(initOverrides);
|
||||
async apiAuthLogoutPost(requestParameters: ApiAuthLogoutPostRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> {
|
||||
await this.apiAuthLogoutPostRaw(requestParameters, initOverrides);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -368,11 +385,13 @@ export class AuthApi extends runtime.BaseAPI {
|
||||
|
||||
/**
|
||||
*/
|
||||
async apiAuthRefreshPostRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<LoginResponse>> {
|
||||
async apiAuthRefreshPostRaw(requestParameters: ApiAuthRefreshPostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<LoginResponse>> {
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
headerParameters['Content-Type'] = 'application/json';
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("Bearer", []);
|
||||
@@ -389,6 +408,7 @@ export class AuthApi extends runtime.BaseAPI {
|
||||
method: 'POST',
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
body: RefreshRequestToJSON(requestParameters['refreshRequest']),
|
||||
}, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) => LoginResponseFromJSON(jsonValue));
|
||||
@@ -396,8 +416,8 @@ export class AuthApi extends runtime.BaseAPI {
|
||||
|
||||
/**
|
||||
*/
|
||||
async apiAuthRefreshPost(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<LoginResponse> {
|
||||
const response = await this.apiAuthRefreshPostRaw(initOverrides);
|
||||
async apiAuthRefreshPost(requestParameters: ApiAuthRefreshPostRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<LoginResponse> {
|
||||
const response = await this.apiAuthRefreshPostRaw(requestParameters, initOverrides);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@ export interface LoginResponse {
|
||||
* @memberof LoginResponse
|
||||
*/
|
||||
accessToken?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof LoginResponse
|
||||
*/
|
||||
refreshToken?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {Date}
|
||||
@@ -57,6 +63,7 @@ export function LoginResponseFromJSONTyped(json: any, ignoreDiscriminator: boole
|
||||
return {
|
||||
|
||||
'accessToken': json['accessToken'] == null ? undefined : json['accessToken'],
|
||||
'refreshToken': json['refreshToken'] == null ? undefined : json['refreshToken'],
|
||||
'expiresAt': json['expiresAt'] == null ? undefined : (new Date(json['expiresAt'])),
|
||||
'mustChangePassword': json['mustChangePassword'] == null ? undefined : json['mustChangePassword'],
|
||||
};
|
||||
@@ -74,6 +81,7 @@ export function LoginResponseToJSONTyped(value?: LoginResponse | null, ignoreDis
|
||||
return {
|
||||
|
||||
'accessToken': value['accessToken'],
|
||||
'refreshToken': value['refreshToken'],
|
||||
'expiresAt': value['expiresAt'] == null ? undefined : ((value['expiresAt']).toISOString()),
|
||||
'mustChangePassword': value['mustChangePassword'],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* OmsorgCore.Api
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import { mapValues } from '../runtime';
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface LogoutRequest
|
||||
*/
|
||||
export interface LogoutRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof LogoutRequest
|
||||
*/
|
||||
refreshToken?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the LogoutRequest interface.
|
||||
*/
|
||||
export function instanceOfLogoutRequest(value: object): value is LogoutRequest {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function LogoutRequestFromJSON(json: any): LogoutRequest {
|
||||
return LogoutRequestFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function LogoutRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): LogoutRequest {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
|
||||
'refreshToken': json['refreshToken'] == null ? undefined : json['refreshToken'],
|
||||
};
|
||||
}
|
||||
|
||||
export function LogoutRequestToJSON(json: any): LogoutRequest {
|
||||
return LogoutRequestToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function LogoutRequestToJSONTyped(value?: LogoutRequest | null, ignoreDiscriminator: boolean = false): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
'refreshToken': value['refreshToken'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* OmsorgCore.Api
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import { mapValues } from '../runtime';
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface RefreshRequest
|
||||
*/
|
||||
export interface RefreshRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof RefreshRequest
|
||||
*/
|
||||
refreshToken?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the RefreshRequest interface.
|
||||
*/
|
||||
export function instanceOfRefreshRequest(value: object): value is RefreshRequest {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function RefreshRequestFromJSON(json: any): RefreshRequest {
|
||||
return RefreshRequestFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function RefreshRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): RefreshRequest {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
|
||||
'refreshToken': json['refreshToken'] == null ? undefined : json['refreshToken'],
|
||||
};
|
||||
}
|
||||
|
||||
export function RefreshRequestToJSON(json: any): RefreshRequest {
|
||||
return RefreshRequestToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function RefreshRequestToJSONTyped(value?: RefreshRequest | null, ignoreDiscriminator: boolean = false): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
'refreshToken': value['refreshToken'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export * from './ForgotPasswordVerifyRequest';
|
||||
export * from './ForgotPasswordVerifyResponse';
|
||||
export * from './LoginRequest';
|
||||
export * from './LoginResponse';
|
||||
export * from './LogoutRequest';
|
||||
export * from './MeResponse';
|
||||
export * from './ModuleType';
|
||||
export * from './OrderResponse';
|
||||
@@ -45,6 +46,7 @@ export * from './PermissionAction';
|
||||
export * from './PermissionDto';
|
||||
export * from './PermissionEffect';
|
||||
export * from './PermissionScope';
|
||||
export * from './RefreshRequest';
|
||||
export * from './ResetUserPasswordRequest';
|
||||
export * from './RolePermissionsResponse';
|
||||
export * from './RoleResponse';
|
||||
|
||||
@@ -2,8 +2,11 @@ import { AuthApi } from "omsorgcore-client-ts";
|
||||
import { configFor, callApi } from "./apiClientHelpers.js";
|
||||
|
||||
// Kapselt /api/auth/* von omsorgCore über den generierten Client (omsorgcore-client-ts).
|
||||
// refresh/logout brauchen keinen Refresh-Token-Parameter mehr - er steckt in der
|
||||
// HttpOnly-Cookie, die der Browser dank credentials:"include" automatisch mitschickt.
|
||||
// refresh/logout brauchen im Browser keinen Refresh-Token-Parameter - er steckt in der
|
||||
// HttpOnly-Cookie, die dank credentials:"include" automatisch mitgeschickt wird. Das Backend
|
||||
// liefert refreshToken trotzdem im Response-Body mit (server-seitige API-Clients wie omsorgWeb
|
||||
// haben keinen Browser-Cookie-Jar und brauchen ihn dort, siehe omsorgCore/CLAUDE.md) - hier
|
||||
// bewusst ignoriert, nie in JS-Variablen/localStorage abgelegt (XSS-Schutz).
|
||||
function toTokenPair(data) {
|
||||
return {
|
||||
accessToken: data.accessToken,
|
||||
@@ -23,14 +26,14 @@ export async function login(username, password) {
|
||||
|
||||
export async function refresh() {
|
||||
const api = new AuthApi(configFor(undefined));
|
||||
const result = await callApi(api.apiAuthRefreshPostRaw());
|
||||
const result = await callApi(api.apiAuthRefreshPostRaw({}));
|
||||
if (!result.ok) return { ok: false, status: result.status };
|
||||
return { ok: true, ...toTokenPair(result.data) };
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
const api = new AuthApi(configFor(undefined));
|
||||
const result = await callApi(api.apiAuthLogoutPostRaw());
|
||||
const result = await callApi(api.apiAuthLogoutPostRaw({}));
|
||||
return { ok: result.ok, status: result.status };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user