Move external secrets behind auth broker
This commit is contained in:
@@ -7,8 +7,6 @@ class AppEnvironment {
|
||||
required this.organizationApiBaseUrl,
|
||||
required this.orgContextApiBaseUrl,
|
||||
required this.orgContextTenantSlug,
|
||||
required this.baronKeyId,
|
||||
required this.baronKeySecret,
|
||||
required this.appVersion,
|
||||
required this.buildTimestamp,
|
||||
});
|
||||
@@ -43,15 +41,6 @@ class AppEnvironment {
|
||||
'TDC114_ORG_CONTEXT_TENANT_SLUG',
|
||||
defaultValue: 'hanmac-family',
|
||||
);
|
||||
const baronKeyId = String.fromEnvironment(
|
||||
'TDC114_BARON_KEY_ID',
|
||||
defaultValue: '',
|
||||
);
|
||||
const baronKeySecret = String.fromEnvironment(
|
||||
'TDC114_BARON_KEY_SECRET',
|
||||
defaultValue: '',
|
||||
);
|
||||
|
||||
return AppEnvironment(
|
||||
ssoBaseUrl: ssoBaseUrl,
|
||||
apiBaseUrl: sharedApiBaseUrl,
|
||||
@@ -70,8 +59,6 @@ class AppEnvironment {
|
||||
: organizationApiBaseOverride)
|
||||
: orgContextApiBaseOverride,
|
||||
orgContextTenantSlug: orgContextTenantSlug,
|
||||
baronKeyId: baronKeyId,
|
||||
baronKeySecret: baronKeySecret,
|
||||
appVersion: const String.fromEnvironment(
|
||||
'APP_VERSION',
|
||||
defaultValue: '0.1.0',
|
||||
@@ -90,8 +77,6 @@ class AppEnvironment {
|
||||
final String organizationApiBaseUrl;
|
||||
final String orgContextApiBaseUrl;
|
||||
final String orgContextTenantSlug;
|
||||
final String baronKeyId;
|
||||
final String baronKeySecret;
|
||||
final String appVersion;
|
||||
final String buildTimestamp;
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ class AuthSessionStore {
|
||||
static const _tokenKey = 'tdc114plus.auth.token';
|
||||
static const _expiresAtKey = 'tdc114plus.auth.expiresAt';
|
||||
static const _userKey = 'tdc114plus.auth.user';
|
||||
static const _orgContextCredentialKey =
|
||||
'tdc114plus.auth.orgContextCredential';
|
||||
static const _pendingRefKey = 'tdc114plus.auth.pending.ref';
|
||||
static const _pendingExpiresAtKey = 'tdc114plus.auth.pending.expiresAt';
|
||||
static const _pendingResendAfterAtKey =
|
||||
@@ -30,17 +28,8 @@ class AuthSessionStore {
|
||||
response.expiresAt.toUtc().toIso8601String(),
|
||||
);
|
||||
await prefs.setString(_userKey, jsonEncode(response.user.toJson()));
|
||||
final orgContextCredential = response.orgContextCredential;
|
||||
if (orgContextCredential == null) {
|
||||
await prefs.remove(_orgContextCredentialKey);
|
||||
} else {
|
||||
await prefs.setString(
|
||||
_orgContextCredentialKey,
|
||||
jsonEncode(orgContextCredential.toJson()),
|
||||
);
|
||||
}
|
||||
debugPrint(
|
||||
'AuthSessionStore.save token=${response.token} tokenLength=${response.token.length} expiresAt=${response.expiresAt.toUtc().toIso8601String()} orgContextCredential=${orgContextCredential != null}',
|
||||
'AuthSessionStore.save token=${response.token} tokenLength=${response.token.length} expiresAt=${response.expiresAt.toUtc().toIso8601String()}',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,7 +102,6 @@ class AuthSessionStore {
|
||||
final token = prefs.getString(_tokenKey);
|
||||
final expiresAtValue = prefs.getString(_expiresAtKey);
|
||||
final userValue = prefs.getString(_userKey);
|
||||
final orgContextCredentialValue = prefs.getString(_orgContextCredentialKey);
|
||||
if (token == null || expiresAtValue == null || userValue == null) {
|
||||
debugPrint(
|
||||
'AuthSessionStore.load missing token=${token != null} expiresAt=${expiresAtValue != null} user=${userValue != null}',
|
||||
@@ -124,12 +112,9 @@ class AuthSessionStore {
|
||||
token: token,
|
||||
expiresAt: DateTime.parse(expiresAtValue),
|
||||
user: LoginUser.fromJson(jsonDecode(userValue) as Map<String, dynamic>),
|
||||
orgContextCredential: _decodeOrgContextCredential(
|
||||
orgContextCredentialValue,
|
||||
),
|
||||
);
|
||||
debugPrint(
|
||||
'AuthSessionStore.load token=${session.token} tokenLength=${session.token.length} expiresAt=${session.expiresAt.toUtc().toIso8601String()} expired=${session.isExpired} orgContextCredential=${session.orgContextCredential != null}',
|
||||
'AuthSessionStore.load token=${session.token} tokenLength=${session.token.length} expiresAt=${session.expiresAt.toUtc().toIso8601String()} expired=${session.isExpired}',
|
||||
);
|
||||
return session;
|
||||
}
|
||||
@@ -139,22 +124,9 @@ class AuthSessionStore {
|
||||
await prefs.remove(_tokenKey);
|
||||
await prefs.remove(_expiresAtKey);
|
||||
await prefs.remove(_userKey);
|
||||
await prefs.remove(_orgContextCredentialKey);
|
||||
await clearPendingLink();
|
||||
debugPrint('AuthSessionStore.clear');
|
||||
}
|
||||
|
||||
OrgContextCredential? _decodeOrgContextCredential(String? value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return OrgContextCredential.fromJsonOrNull(jsonDecode(value));
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class StoredAuthSession {
|
||||
@@ -162,13 +134,11 @@ class StoredAuthSession {
|
||||
required this.token,
|
||||
required this.expiresAt,
|
||||
required this.user,
|
||||
this.orgContextCredential,
|
||||
});
|
||||
|
||||
final String token;
|
||||
final DateTime expiresAt;
|
||||
final LoginUser user;
|
||||
final OrgContextCredential? orgContextCredential;
|
||||
|
||||
bool get isExpired => !expiresAt.isAfter(DateTime.now().toUtc());
|
||||
}
|
||||
|
||||
@@ -101,14 +101,12 @@ class PhoneLoginResponse {
|
||||
required this.token,
|
||||
required this.expiresAt,
|
||||
required this.user,
|
||||
this.orgContextCredential,
|
||||
});
|
||||
|
||||
final String status;
|
||||
final String token;
|
||||
final DateTime expiresAt;
|
||||
final LoginUser user;
|
||||
final OrgContextCredential? orgContextCredential;
|
||||
|
||||
factory PhoneLoginResponse.fromJson(Map<String, dynamic> json) {
|
||||
return PhoneLoginResponse(
|
||||
@@ -116,9 +114,6 @@ class PhoneLoginResponse {
|
||||
token: json['token'] as String? ?? json['accessToken'] as String? ?? '',
|
||||
expiresAt: DateTime.parse(json['expiresAt'] as String),
|
||||
user: LoginUser.fromJson(json['user'] as Map<String, dynamic>? ?? {}),
|
||||
orgContextCredential: OrgContextCredential.fromJsonOrNull(
|
||||
json['orgContextCredential'] ?? json['org_context'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,83 +123,10 @@ class PhoneLoginResponse {
|
||||
'token': token,
|
||||
'expiresAt': expiresAt.toUtc().toIso8601String(),
|
||||
'user': user.toJson(),
|
||||
if (orgContextCredential != null)
|
||||
'orgContextCredential': orgContextCredential!.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class OrgContextCredential {
|
||||
const OrgContextCredential({
|
||||
required this.baseUrl,
|
||||
required this.tenantSlug,
|
||||
required this.keyId,
|
||||
required this.keySecret,
|
||||
this.expiresAt,
|
||||
});
|
||||
|
||||
final String baseUrl;
|
||||
final String tenantSlug;
|
||||
final String keyId;
|
||||
final String keySecret;
|
||||
final DateTime? expiresAt;
|
||||
|
||||
bool get isUsable {
|
||||
return baseUrl.trim().isNotEmpty &&
|
||||
tenantSlug.trim().isNotEmpty &&
|
||||
keyId.trim().isNotEmpty &&
|
||||
keySecret.trim().isNotEmpty &&
|
||||
(expiresAt == null || expiresAt!.isAfter(DateTime.now().toUtc()));
|
||||
}
|
||||
|
||||
factory OrgContextCredential.fromJson(Map<String, dynamic> json) {
|
||||
return OrgContextCredential(
|
||||
baseUrl: _readString(json, const ['baseUrl', 'base_url']),
|
||||
tenantSlug: _readString(json, const ['tenantSlug', 'tenant_slug']),
|
||||
keyId: _readString(json, const ['keyId', 'key_id']),
|
||||
keySecret: _readString(json, const ['keySecret', 'key_secret']),
|
||||
expiresAt: _readDate(json['expiresAt'] ?? json['expires_at']),
|
||||
);
|
||||
}
|
||||
|
||||
static OrgContextCredential? fromJsonOrNull(Object? value) {
|
||||
if (value is! Map) {
|
||||
return null;
|
||||
}
|
||||
final credential = OrgContextCredential.fromJson(
|
||||
Map<String, dynamic>.from(value),
|
||||
);
|
||||
return credential.isUsable ? credential : null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'baseUrl': baseUrl,
|
||||
'tenantSlug': tenantSlug,
|
||||
'keyId': keyId,
|
||||
'keySecret': keySecret,
|
||||
if (expiresAt != null) 'expiresAt': expiresAt!.toUtc().toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
static String _readString(Map<String, dynamic> json, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final value = json[key];
|
||||
if (value is String && value.trim().isNotEmpty) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
static DateTime? _readDate(Object? value) {
|
||||
if (value is! String || value.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.tryParse(value)?.toUtc();
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneLoginLinkInitRequest {
|
||||
const PhoneLoginLinkInitRequest({
|
||||
required this.phoneNumber,
|
||||
|
||||
@@ -16,8 +16,6 @@ class OrgContextApiClient {
|
||||
const OrgContextApiClient({
|
||||
required this.httpClient,
|
||||
required this.baseUri,
|
||||
required this.keyId,
|
||||
required this.keySecret,
|
||||
required this.tenantSlug,
|
||||
this.sessionStore,
|
||||
this.timeout = const Duration(seconds: 15),
|
||||
@@ -28,8 +26,6 @@ class OrgContextApiClient {
|
||||
|
||||
final http.Client httpClient;
|
||||
final Uri baseUri;
|
||||
final String keyId;
|
||||
final String keySecret;
|
||||
final String tenantSlug;
|
||||
final AuthSessionStore? sessionStore;
|
||||
final Duration timeout;
|
||||
@@ -91,27 +87,13 @@ class OrgContextApiClient {
|
||||
String? tenantSlug,
|
||||
}) async {
|
||||
final session = await _session();
|
||||
final sessionCredential = session?.orgContextCredential;
|
||||
final requestedTenantSlug = tenantSlug?.trim();
|
||||
if (sessionCredential != null && sessionCredential.isUsable) {
|
||||
return _OrgContextCredentialSnapshot(
|
||||
baseUri: Uri.parse(sessionCredential.baseUrl),
|
||||
tenantSlug: requestedTenantSlug == null || requestedTenantSlug.isEmpty
|
||||
? sessionCredential.tenantSlug
|
||||
: requestedTenantSlug,
|
||||
keyId: sessionCredential.keyId,
|
||||
keySecret: sessionCredential.keySecret,
|
||||
appSessionToken: session?.token ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
return _OrgContextCredentialSnapshot(
|
||||
baseUri: baseUri,
|
||||
tenantSlug: requestedTenantSlug == null || requestedTenantSlug.isEmpty
|
||||
? this.tenantSlug
|
||||
: requestedTenantSlug,
|
||||
keyId: keyId,
|
||||
keySecret: keySecret,
|
||||
appSessionToken: session?.token ?? '',
|
||||
);
|
||||
}
|
||||
@@ -141,10 +123,6 @@ class OrgContextApiClient {
|
||||
'accept': 'application/json',
|
||||
if (appSessionToken.isNotEmpty) 'Authorization': 'Bearer $appSessionToken',
|
||||
if (appSessionToken.isNotEmpty) 'X-App-Session': appSessionToken,
|
||||
if (credential.keyId.trim().isNotEmpty)
|
||||
'X-Baron-Key-ID': credential.keyId.trim(),
|
||||
if (credential.keySecret.trim().isNotEmpty)
|
||||
'X-Baron-Key-Secret': credential.keySecret.trim(),
|
||||
};
|
||||
debugPrint(
|
||||
'OrgContextApiClient._headers headers=$headers tenantSlug=${credential.tenantSlug} base=${credential.baseUri}',
|
||||
@@ -458,8 +436,6 @@ final orgContextApiClientProvider = Provider<OrgContextApiClient>((ref) {
|
||||
return OrgContextApiClient(
|
||||
httpClient: ref.watch(httpClientProvider),
|
||||
baseUri: Uri.parse(environment.orgContextApiBaseUrl),
|
||||
keyId: environment.baronKeyId,
|
||||
keySecret: environment.baronKeySecret,
|
||||
tenantSlug: environment.orgContextTenantSlug,
|
||||
sessionStore: ref.watch(authSessionStoreProvider),
|
||||
);
|
||||
@@ -475,22 +451,18 @@ class _OrgContextCredentialSnapshot {
|
||||
const _OrgContextCredentialSnapshot({
|
||||
required this.baseUri,
|
||||
required this.tenantSlug,
|
||||
required this.keyId,
|
||||
required this.keySecret,
|
||||
required this.appSessionToken,
|
||||
});
|
||||
|
||||
final Uri baseUri;
|
||||
final String tenantSlug;
|
||||
final String keyId;
|
||||
final String keySecret;
|
||||
final String appSessionToken;
|
||||
|
||||
String get cacheKey {
|
||||
return [
|
||||
baseUri.toString(),
|
||||
tenantSlug,
|
||||
appSessionToken.isEmpty ? keyId : appSessionToken,
|
||||
appSessionToken,
|
||||
].join('|');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,13 +22,6 @@ void main() {
|
||||
tenantName: 'Hanmac',
|
||||
tenantSlug: 'hanmac',
|
||||
),
|
||||
orgContextCredential: OrgContextCredential(
|
||||
baseUrl: 'https://sadmin.hmac.kr',
|
||||
tenantSlug: 'hanmac-family',
|
||||
keyId: 'session-key-id',
|
||||
keySecret: 'session-key-secret',
|
||||
expiresAt: DateTime.parse('2099-07-02T13:00:00Z'),
|
||||
),
|
||||
);
|
||||
|
||||
await store.save(response);
|
||||
@@ -36,8 +29,6 @@ void main() {
|
||||
|
||||
expect(loaded?.token, 'session-token');
|
||||
expect(loaded?.user.name, 'User One');
|
||||
expect(loaded?.orgContextCredential?.keyId, 'session-key-id');
|
||||
expect(loaded?.orgContextCredential?.keySecret, 'session-key-secret');
|
||||
|
||||
await store.clear();
|
||||
expect(await store.load(), isNull);
|
||||
|
||||
@@ -22,14 +22,12 @@ void main() {
|
||||
expect(request.url.queryParameters['tenantSlug'], 'hanmac-family');
|
||||
expect(request.url.queryParameters['includeUsers'], 'true');
|
||||
expect(request.url.queryParameters['includeUserIds'], 'true');
|
||||
expect(request.headers['X-Baron-Key-ID'], 'key-id');
|
||||
expect(request.headers['X-Baron-Key-Secret'], 'key-secret');
|
||||
expect(request.headers.containsKey('X-Baron-Key-ID'), isFalse);
|
||||
expect(request.headers.containsKey('X-Baron-Key-Secret'), isFalse);
|
||||
|
||||
return _jsonResponse(_orgContextResponse());
|
||||
}),
|
||||
baseUri: Uri.parse('https://sadmin.hmac.kr'),
|
||||
keyId: 'key-id',
|
||||
keySecret: 'key-secret',
|
||||
tenantSlug: 'hanmac-family',
|
||||
);
|
||||
|
||||
@@ -49,7 +47,7 @@ void main() {
|
||||
expect(is3.totalMemberCount, 2);
|
||||
});
|
||||
|
||||
test('uses session org-context credential before env fallback', () async {
|
||||
test('does not persist or send session org-context credentials', () async {
|
||||
const store = AuthSessionStore();
|
||||
await store.save(
|
||||
PhoneLoginResponse(
|
||||
@@ -64,27 +62,19 @@ void main() {
|
||||
tenantName: '',
|
||||
tenantSlug: '',
|
||||
),
|
||||
orgContextCredential: const OrgContextCredential(
|
||||
baseUrl: 'https://session.example.test',
|
||||
tenantSlug: 'session-family',
|
||||
keyId: 'session-key-id',
|
||||
keySecret: 'session-key-secret',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final client = OrgContextApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
expect(request.url.host, 'session.example.test');
|
||||
expect(request.url.queryParameters['tenantSlug'], 'session-family');
|
||||
expect(request.url.host, 'env.example.test');
|
||||
expect(request.url.queryParameters['tenantSlug'], 'env-family');
|
||||
expect(request.headers['Authorization'], 'Bearer session-token');
|
||||
expect(request.headers['X-Baron-Key-ID'], 'session-key-id');
|
||||
expect(request.headers['X-Baron-Key-Secret'], 'session-key-secret');
|
||||
expect(request.headers.containsKey('X-Baron-Key-ID'), isFalse);
|
||||
expect(request.headers.containsKey('X-Baron-Key-Secret'), isFalse);
|
||||
return _jsonResponse(_orgContextResponse());
|
||||
}),
|
||||
baseUri: Uri.parse('https://env.example.test'),
|
||||
keyId: 'env-key-id',
|
||||
keySecret: 'env-key-secret',
|
||||
tenantSlug: 'env-family',
|
||||
sessionStore: store,
|
||||
);
|
||||
@@ -99,8 +89,6 @@ void main() {
|
||||
return _jsonResponse(_orgContextResponse());
|
||||
}),
|
||||
baseUri: Uri.parse('https://sadmin.hmac.kr'),
|
||||
keyId: 'key-id',
|
||||
keySecret: 'key-secret',
|
||||
tenantSlug: 'hanmac-family',
|
||||
);
|
||||
|
||||
@@ -115,8 +103,6 @@ void main() {
|
||||
return _jsonResponse(_orgContextResponse());
|
||||
}),
|
||||
baseUri: Uri.parse('https://114-auth.hmac.kr'),
|
||||
keyId: '',
|
||||
keySecret: '',
|
||||
tenantSlug: 'hanmac-family',
|
||||
);
|
||||
|
||||
@@ -155,8 +141,6 @@ void main() {
|
||||
return _jsonResponse(_orgContextResponse());
|
||||
}),
|
||||
baseUri: Uri.parse('https://114-auth.hmac.kr'),
|
||||
keyId: '',
|
||||
keySecret: '',
|
||||
tenantSlug: 'hanmac-family',
|
||||
sessionStore: store,
|
||||
);
|
||||
@@ -173,8 +157,6 @@ void main() {
|
||||
return _jsonResponse(_orgContextResponse());
|
||||
}),
|
||||
baseUri: Uri.parse('https://114-auth.hmac.kr'),
|
||||
keyId: '',
|
||||
keySecret: '',
|
||||
tenantSlug: 'hanmac-family',
|
||||
);
|
||||
|
||||
@@ -184,27 +166,22 @@ void main() {
|
||||
expect(requestCount, 1);
|
||||
});
|
||||
|
||||
test(
|
||||
'falls back to env org-context credential without session credential',
|
||||
() async {
|
||||
final client = OrgContextApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
expect(request.url.host, 'env.example.test');
|
||||
expect(request.url.queryParameters['tenantSlug'], 'env-family');
|
||||
expect(request.headers['X-Baron-Key-ID'], 'env-key-id');
|
||||
expect(request.headers['X-Baron-Key-Secret'], 'env-key-secret');
|
||||
return _jsonResponse(_orgContextResponse());
|
||||
}),
|
||||
baseUri: Uri.parse('https://env.example.test'),
|
||||
keyId: 'env-key-id',
|
||||
keySecret: 'env-key-secret',
|
||||
tenantSlug: 'env-family',
|
||||
sessionStore: const AuthSessionStore(),
|
||||
);
|
||||
test('uses configured auth-server org-context endpoint without Baron key headers', () async {
|
||||
final client = OrgContextApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
expect(request.url.host, 'env.example.test');
|
||||
expect(request.url.queryParameters['tenantSlug'], 'env-family');
|
||||
expect(request.headers.containsKey('X-Baron-Key-ID'), isFalse);
|
||||
expect(request.headers.containsKey('X-Baron-Key-Secret'), isFalse);
|
||||
return _jsonResponse(_orgContextResponse());
|
||||
}),
|
||||
baseUri: Uri.parse('https://env.example.test'),
|
||||
tenantSlug: 'env-family',
|
||||
sessionStore: const AuthSessionStore(),
|
||||
);
|
||||
|
||||
await client.fetchOrgContext();
|
||||
},
|
||||
);
|
||||
await client.fetchOrgContext();
|
||||
});
|
||||
|
||||
test(
|
||||
'remote directory repository filters org-context employees locally',
|
||||
@@ -215,8 +192,6 @@ void main() {
|
||||
return _jsonResponse(_orgContextResponse());
|
||||
}),
|
||||
baseUri: Uri.parse('https://sadmin.hmac.kr'),
|
||||
keyId: 'key-id',
|
||||
keySecret: 'key-secret',
|
||||
tenantSlug: 'hanmac-family',
|
||||
);
|
||||
final repository = RemoteDirectoryRepository(orgContextApiClient: client);
|
||||
@@ -238,8 +213,6 @@ void main() {
|
||||
return _jsonResponse(_orgContextResponse());
|
||||
}),
|
||||
baseUri: Uri.parse('https://sadmin.hmac.kr'),
|
||||
keyId: 'key-id',
|
||||
keySecret: 'key-secret',
|
||||
tenantSlug: 'hanmac-family',
|
||||
);
|
||||
final repository = RemoteDirectoryRepository(orgContextApiClient: client);
|
||||
@@ -264,8 +237,6 @@ void main() {
|
||||
return _jsonResponse(_orgContextResponse());
|
||||
}),
|
||||
baseUri: Uri.parse('https://sadmin.hmac.kr'),
|
||||
keyId: 'key-id',
|
||||
keySecret: 'key-secret',
|
||||
tenantSlug: 'hanmac-family',
|
||||
);
|
||||
final repository = RemoteDirectoryRepository(orgContextApiClient: client);
|
||||
|
||||
Reference in New Issue
Block a user