public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: "Shan Shaji" <s.shaji@proxmox.com>
To: "Azharul Haque" <haque@azharul.com>, <pve-devel@lists.proxmox.com>
Subject: Re: [PATCH dart-api-client v2 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers
Date: Thu, 20 Aug 2026 11:48:59 +0200	[thread overview]
Message-ID: <DKTODQ6KIVIB.135Q0E7N3Q6GS@proxmox.com> (raw)
In-Reply-To: <20260810144713.75806-3-haque@azharul.com>

On Mon Aug 10, 2026 at 4:47 PM CEST, Azharul Haque wrote:
> Add openIdAuthUrl() and openIdLogin(), mirroring the existing
> authenticate()/accessDomains() functions used by the login form
> before an authenticated ProxmoxApiClient exists.
>
> openIdAuthUrl() requests the provider's authorization URL for a
> realm from /access/openid/auth-url. openIdLogin() exchanges the
> state/code obtained from the provider's redirect for a PVE ticket
> via /access/openid/login, the same way authenticate() does for
> password realms.
>
> The OpenID login response carries the authenticated username in its
> body rather than it being known upfront by the caller, so
> handleOpenIdLoginResponse() is added alongside the existing
> handleAccessTicketResponse()/handleTfaChallengeResponse() to build
> Credentials from it.
>
> Signed-off-by: Azharul Haque <haque@azharul.com>
> ---
>  lib/src/authenticate.dart           | 85 +++++++++++++++++++++++++++++
>  lib/src/handle_ticket_response.dart | 36 ++++++++++++
>  test/test.dart                      | 20 +++++++
>  3 files changed, 141 insertions(+)
>
> diff --git a/lib/src/authenticate.dart b/lib/src/authenticate.dart
> index 7bd9cef..e2d76a1 100644
> --- a/lib/src/authenticate.dart
> +++ b/lib/src/authenticate.dart
> @@ -72,6 +72,91 @@ Future<ProxmoxApiClient> authenticate(
>    }
>  }
>  
> +/// Requests the provider's authorization URL for an OpenID Connect realm.
> +///
> +/// [redirectUrl] must match a redirect URI registered with the realm's
> +/// OpenID provider, and is where the provider sends the user back to after
> +/// they authenticate (carrying `state` and `code` query parameters).
> +Future<String> openIdAuthUrl(
> +  String realm,
> +  Uri apiBaseUrl,
> +  Uri redirectUrl,
> +  bool validateSSL, {
> +  http.Client? httpClient,
> +}) async {
> +  httpClient ??= getCustomIOHttpClient(validateSSL: validateSSL);
> +
> +  var body = {
> +    'realm': realm,
> +    'redirect-url': redirectUrl.toString(),
> +  };
> +
> +  try {
> +    final path = '/api2/json/access/openid/auth-url';
> +    final response = await httpClient
> +        .post(apiBaseUrl.replace(path: path), body: body)
> +        .timeout(Duration(seconds: 25));
> +
> +    response.validate(true);
> +
> +    return jsonDecode(response.body)['data'] as String;
> +  } on NSErrorClientException catch (e) {
> +    if (e.error.code == -1202) {
> +      throw HandshakeException(e.message);
> +    }
> +    rethrow;
> +  } on http.ClientException catch (e) {
> +    if (e.message.contains('net::ERR_CERT_AUTHORITY_INVALID')) {
> +      throw HandshakeException(e.message);
> +    }
> +    rethrow;
> +  }
> +}
> +
> +/// Exchanges the `state`/`code` obtained from the OpenID provider's redirect
> +/// for a Proxmox VE ticket, mirroring what [authenticate] does for password
> +/// realms.
> +Future<ProxmoxApiClient> openIdLogin(
> +  String state,
> +  String code,
> +  Uri apiBaseUrl,
> +  Uri redirectUrl,
> +  bool validateSSL, {
> +  http.Client? httpClient,
> +}) async {
> +  httpClient ??= getCustomIOHttpClient(validateSSL: validateSSL);
> +
> +  var body = {
> +    'state': state,
> +    'code': code,
> +    'redirect-url': redirectUrl.toString(),
> +  };
> +
> +  try {
> +    final path = '/api2/json/access/openid/login';
> +    final response = await httpClient
> +        .post(apiBaseUrl.replace(path: path), body: body)
> +        .timeout(Duration(seconds: 25));
> +
> +    final credentials = handleOpenIdLoginResponse(response, apiBaseUrl);
> +
> +    return ProxmoxApiClient(
> +      credentials,
> +      httpClient: httpClient,
> +    );
> +  } on NSErrorClientException catch (e) {
> +    if (e.error.code == -1202) {
> +      throw HandshakeException(e.message);
> +    }
> +    rethrow;
> +  } on http.ClientException catch (e) {
> +    if (e.message.contains('net::ERR_CERT_AUTHORITY_INVALID')) {
> +      throw HandshakeException(e.message);
> +    }
> +    rethrow;
> +  }
> +}
> +
>  Future<List<PveAccessDomainModel?>> accessDomains(
>    Uri apiBaseUrl,
>    bool validateSSL, {
> diff --git a/lib/src/handle_ticket_response.dart b/lib/src/handle_ticket_response.dart
> index ba2128f..a43aed0 100644
> --- a/lib/src/handle_ticket_response.dart
> +++ b/lib/src/handle_ticket_response.dart
> @@ -39,6 +39,42 @@ Credentials handleAccessTicketResponse(
>    );
>  }
>  
> +Credentials handleOpenIdLoginResponse(
> +    http.Response response, Uri apiBaseUrl) {
> +  response.validate(false);


    > +  final bodyJson = jsonDecode(response.body)['data'];
    > +
    > +  final ticket = bodyJson['ticket'];
    > +
    > +  final csrfToken = bodyJson['CSRFPreventionToken'];
    > +
    > +  final username = bodyJson['username'];
    > +
    > +  final ticketRegex = RegExp(r'(PVE|PMG)(?:QUAR)?:(?:(\S+):)?([A-Z0-9]{8})::')

    I know, you copied this regex from the handleAccessTicketResponse function. But IMHO,
    the first group don't need to match for PMG as well. It could just be PVE.

      final ticketRegex = RegExp(r'(PVE)(?:QUAR)?:(?:(\S+):)?([A-Z0-9]{8})::')

    > +      .firstMatch(bodyJson['ticket'])!;
    > +
    > +  final time = DateTime.fromMillisecondsSinceEpoch(
    > +      int.parse(ticketRegex.group(3)!, radix: 16) * 1000);
    > +
    > +  TfaChallenge? tfa;
    > +  if (ticket.startsWith('PVE:!tfa!')) {
    > +    tfa = TfaChallenge.fromJson(
    > +        jsonDecode(Uri.decodeComponent(ticket.substring(9).split(':')[0])));
    > +  } else if (bodyJson['NeedTFA'] != null && bodyJson['NeedTFA'] == 1) {
    > +    tfa = TfaChallenge.legacy();
    > +  }

  This block is common in both handleAccessTicketResponse and handleOpenIdLogiResponse
  functions. IMHO, we could seperate this into its own function.

> +  return Credentials(
> +    apiBaseUrl,
> +    username,
> +    ticket: ticket,
> +    csrfToken: csrfToken,
> +    expiration: time,
> +    tfa: tfa,
> +  );
> +}
> +
>  Credentials handleTfaChallengeResponse(
>      http.Response response, Credentials pendingTfaCredentials) {
>    response.validate(false);
> diff --git a/test/test.dart b/test/test.dart
> index 23368a2..86c2272 100644
> --- a/test/test.dart
> +++ b/test/test.dart
> @@ -49,5 +49,25 @@ void main() {
>                    DateTime.fromMillisecondsSinceEpoch(
>                        int.parse('5DF8EC22', radix: 16) * 1000)));
>      });
> +
> +    test('valid openid login response extraction', () {
> +      final ticket =
> +          'PVE:jdoe@keycloak:5DF8EC22::STV4HNO1wplmsyMDM5s6SUsU4cS7sBBBw+HOCEhSSV+6WGtz3zwIzHqBhq/ziJoBs7NqqyLXG4wn9jXJCMdYht+ndqwxtdFQsUNOF1Q/eTWwcyl+Q1fmPNOIIUoxMY8OqGBVozgIimiAJxdqm+2SJnrPEmlJge6m3yf/OEVAkKFCfRMOtSuyVnIbuLx6h6obvezBUP5+ZHzeTMmmXcH4rOsOKgW9XfwryLHbkjjq9Ennx0xjQaBD9Bo5ERquY0hNmWcdPC/p7ZzILTr4xH9sJe9Na2z6GhgJyTgOCAMengyIegySMq7IKIkmsp8odF4/iIC3005/XLF4w/DjPYQUMA==';
> +      final csrfToken = '5DF8EDEC:/bb44xdHyVQDo2eD/8ty0WVXwMgwt1HjhVHLZX2YbxQ';
> +      var response = http.Response(
> +          '{"data":{"clustername":"testcluster","username":"jdoe@keycloak","CSRFPreventionToken":"5DF8EDEC:/bb44xdHyVQDo2eD/8ty0WVXwMgwt1HjhVHLZX2YbxQ","cap":{},"ticket":"$ticket"}}',
> +          200);
> +      expect(
> +          handleOpenIdLoginResponse(response, dummyEndpoint),
> +          isA<Credentials>()
> +              .having((e) => e.username, 'Username', 'jdoe@keycloak')
> +              .having((e) => e.ticket, 'Ticket', ticket)
> +              .having((e) => e.csrfToken, 'CSRF Token', csrfToken)
> +              .having(
> +                  (e) => e.expiration,
> +                  'Token expiration time',
> +                  DateTime.fromMillisecondsSinceEpoch(
> +                      int.parse('5DF8EC22', radix: 16) * 1000)));
> +    });
>    });
>  }

If you would like to make the changes I mentioned, please feel free to do
that, else I can do it in a seperate series. 




  reply	other threads:[~2026-08-20  9:49 UTC|newest]

Thread overview: 19+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-10  5:40 [PATCH 0/2] android: register OpenID Connect callback activity for #4281 Azharul Haque
2026-08-10  5:40 ` [PATCH 1/2] fix #4281: android: register OpenID Connect callback activity Azharul Haque
2026-08-10  5:40 ` [PATCH 2/2] fix #4281: android: match renamed OpenID callback scheme Azharul Haque
2026-08-10 13:59 ` [PATCH 0/2] android: register OpenID Connect callback activity for #4281 Shan Shaji
2026-08-10 14:47   ` [PATCH v2 0/7] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque
2026-08-10 14:47     ` [PATCH dart-api-client v2 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` Azharul Haque
2026-08-20  9:17       ` Shan Shaji
     [not found]         ` <CAFCWXbiQxG4p2U+beMjSi7Gaz5qC-vCzXW1AH3Ys-ApOW0S83w@mail.gmail.com>
2026-08-20 14:39           ` Shan Shaji
     [not found]             ` <CAFCWXbhnW0o1VcUimwk6pUB3VqCaQw31BQpuHjDtKF8LGeB-fg@mail.gmail.com>
2026-08-21  8:00               ` Shan Shaji
2026-08-10 14:47     ` [PATCH dart-api-client v2 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers Azharul Haque
2026-08-20  9:48       ` Shan Shaji [this message]
2026-08-10 14:47     ` [PATCH login-manager v2 1/3] fix #4281: ui: add OpenID Connect login flow to login form Azharul Haque
2026-08-20 12:31       ` Shan Shaji
2026-08-10 14:47     ` [PATCH login-manager v2 2/3] fix #4281: ui: fix stale Continue button state on realm switch Azharul Haque
2026-08-10 14:47     ` [PATCH login-manager v2 3/3] fix #4281: ui: use a namespaced OpenID callback scheme Azharul Haque
2026-08-20 14:28       ` Shan Shaji
2026-08-10 14:47     ` [PATCH flutter-frontend v2 1/2] fix #4281: android: register OpenID Connect callback activity Azharul Haque
2026-08-20 14:01       ` Shan Shaji
2026-08-10 14:47     ` [PATCH flutter-frontend v2 2/2] fix #4281: android: match renamed OpenID callback scheme Azharul Haque

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=DKTODQ6KIVIB.135Q0E7N3Q6GS@proxmox.com \
    --to=s.shaji@proxmox.com \
    --cc=haque@azharul.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal