fix(oidc): the fake provider takes the client's credentials form-encoded

The module sends the client's ID and secret form-encoded inside Basic
authentication, as RFC 6749 (2.3.1) has them; the fake compared the header
with them raw, so a host testing with a secret holding a space or a colon
saw every code refused. A test now holds the module to the encoding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-23 21:03:15 +02:00
commit a0842e401d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 30 additions and 2 deletions

View file

@ -157,8 +157,11 @@ final class FakeProvider {
}
http.Response _token(http.Request request) {
final basic =
'Basic ${base64.encode(utf8.encode('$clientId:$clientSecret'))}';
// The client's ID and secret form-encoded first (RFC 6749, 2.3.1).
final credentials =
'${Uri.encodeQueryComponent(clientId)}:'
'${Uri.encodeQueryComponent(clientSecret)}';
final basic = 'Basic ${base64.encode(utf8.encode(credentials))}';
final form = request.bodyFields;
final code = _codes.remove(form['code']);
if (request.headers['authorization'] != basic ||

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:clock/clock.dart';
@ -699,6 +700,30 @@ void main() {
expect(fake.discoveries, 0);
});
test("sends the client's ID and secret form-encoded, as OAuth 2.0 "
'has them in Basic authentication (RFC 6749, 2.3.1)', () async {
useProvider(
FakeProvider.simpleLogin(
key: key,
clientId: 'halkyon:learn',
clientSecret: 'a secret/+&',
callbackUrl: Uri.parse('https://learn.example/simplelogin-callback'),
),
);
final start = await idp.begin(session, provider: 'simplelogin');
await idp.verify(
session,
attempt: start.attempt,
code: fake.authorize(start.authorizationUrl, subject: 'sl-1'),
);
expect(
fake.tokenRequests.single.headers['authorization'],
'Basic ${base64.encode(utf8.encode('halkyon%3Alearn:a+secret%2F%2B%26'))}',
);
});
test('forgets attempts whose time ran out', () async {
await idp.begin(session, provider: 'fake');
final (:attempt, :code) = await allowed('42');