Move external secrets behind auth broker

This commit is contained in:
Codex
2026-07-20 14:19:55 +09:00
parent ef0d53aa87
commit cb36612b88
14 changed files with 55 additions and 240 deletions
@@ -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);