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
4 changes: 4 additions & 0 deletions packages/stream_core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

- Added `teams` field to `User` class.

### 🐛 Bug Fixes

- `AuthInterceptor` now sets the `user_id` query parameter from the resolved token's own user id instead of `TokenManager.userId`. This keeps REST calls consistent for token providers that can resolve to a different id than requested (e.g. guest token exchanges).

## 0.4.0

### 💥 BREAKING CHANGES
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ class AuthInterceptor extends QueuedInterceptor {
try {
final token = await _tokenManager.getToken();

options.queryParameters['user_id'] = _tokenManager.userId;
// Use the resolved token's own user id rather than
// `_tokenManager.userId`: some token providers (e.g. guest exchanges)
// can resolve to a different id than the one originally requested, and
// this must stay consistent with the identity in the `Authorization`
// header below.
options.queryParameters['user_id'] = token.userId;
Comment on lines +26 to +31

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion the token should always match the id set in the token manager. if its different we should expire the current token and get a new token for the user id set in the manager.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options.headers['Authorization'] = token.rawValue;
options.headers['stream-auth-type'] = token.authType.headerValue;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'dart:convert';

import 'package:stream_core/stream_core.dart';
import 'package:test/test.dart';

// A minimal HttpClientAdapter that captures the outgoing RequestOptions and
// always responds with an empty successful response.
class _CapturingHttpClientAdapter implements HttpClientAdapter {
RequestOptions? lastRequest;

@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? cancelFuture,
) async {
lastRequest = options;
return ResponseBody.fromString(
'{}',
200,
headers: {
Headers.contentTypeHeader: [Headers.jsonContentType],
},
);
}

@override
void close({bool force = false}) {}
}

UserToken _generateTestUserToken(String userId) {
String b64UrlNoPad(Object jsonObj) {
final bytes = utf8.encode(jsonEncode(jsonObj));
return base64Url.encode(bytes).replaceAll('=', '');
}

final header = {'alg': 'none', 'typ': 'JWT'};
final payload = {'user_id': userId};

final jwt = '${b64UrlNoPad(header)}.${b64UrlNoPad(payload)}.';
return UserToken(jwt);
}

void main() {
group('AuthInterceptor', () {
test(
"uses the resolved token's own user id for the user_id query "
'parameter, not the id the TokenManager was constructed with',
() async {
// Simulates a token provider (e.g. a guest exchange) that resolves
// to a different id than the one originally requested.
final tokenManager = TokenManager(
userId: 'requested-id',
tokenProvider: TokenProvider.dynamic(
(_) async => _generateTestUserToken('server-assigned-id'),
),
);

final dio = Dio(BaseOptions(baseUrl: 'https://example.com'));
final adapter = _CapturingHttpClientAdapter();
dio.httpClientAdapter = adapter;
dio.interceptors.add(AuthInterceptor(dio, tokenManager));

await dio.get<void>('/test');

expect(
adapter.lastRequest?.queryParameters['user_id'],
'server-assigned-id',
);
},
);

test(
'matches the TokenManager userId when the token resolves to the '
'same id (regular/anonymous users)',
() async {
final tokenManager = TokenManager(
userId: 'user-123',
tokenProvider: TokenProvider.static(
_generateTestUserToken('user-123'),
),
);

final dio = Dio(BaseOptions(baseUrl: 'https://example.com'));
final adapter = _CapturingHttpClientAdapter();
dio.httpClientAdapter = adapter;
dio.interceptors.add(AuthInterceptor(dio, tokenManager));

await dio.get<void>('/test');

expect(adapter.lastRequest?.queryParameters['user_id'], 'user-123');
},
);
});
}
Loading