WIP: act as a Matrix OIDC auth provider #810
Labels
No labels
Bug
Cherry-picking
Database
Dependencies
Documentation
Enhancement
Good first issue
Help wanted
Inherited
Matrix/Administration
Matrix/Appservices
Matrix/Auth
Matrix/Client
Matrix/Core
Matrix/Federation
Matrix/MSC
Matrix/Media
Meta
Meta/Packaging
Priority
Blocking
Priority
High
Priority
Low
Security
Status
Confirmed
Status
Duplicate
Status
Invalid
Status
Needs Investigation
Wont fix
old/ci/cd
old/rust
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: continuwuation/continuwuity#810
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "lafleur/continuwuity:as-oidc-provider"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
This MR proposes to implement acting as an OIDC authentication provider following the expectations of
The OIDC token flow implies an OAuth transaction implemented with the
oxide-auth
crate. An enhancement could be to add html templates support for the login and consentment pages (and the future account registration/management pages).The token flow was tested against areweoidcyet.com, and passes all the steps. Unfortunately, I was unable to use that flow with any app supporting new-style authentication.
MSC4254 (token revocation) should be straightforward to implement, I'll do it soon.
These MSCs form MSC3861, that recently landed in the spec - see their blog.
Thank you for the PR! I'd like to get #801 merged, then we can work on a basic HTML login page, then we can get login working with Element Web. & Element X as our test targets. We'll also need to add the database tables for dynamic client registration - as it's entirely new tables, that shouldn't cause any database compatibility issues with other conduits, but we'll need to pay attention if any modifications are required elsewhere to the database in the process.
I also want to see if we can move the routing stuff to ruwuma, although that's a lower-priority change.
All that said, this is an excellent start, thank you again! I'm quite happy with using
oxide-auth
here to avoid maintaining that code ourselves, and it looks well-maintained from a quick glance.Great ! I noticed you added the askama crate in #801, I should be able to update this MR once you merge it to use its templating system in the current login page - and perhaps the css you added, if it's ment to be site-wide.
Currently I implemented the clients db as a volatile struct using oxide-auth's tooling. This means it's reset at server shutdown. I thought it was fine because I thought clients had to register on each connection anyway, but now I understand that they expect their registration to stay valid across server restart. So yes, I guess we have to add new tables.
BTW, I believe the implementation of msc2965 implies that user registration takes place at a new web endpoint,
/_matrix/static/client/register
(which is a bit confusing for an user registration endpoint if you ask me). So I'd like to implement that endpoint too, even if I couldn't find the relevant msc.That all sounds good to me! The CSS in there at the moment is a pretty rough draft, but ideally, we'd have one piece of base CSS (although perhaps split across multiple actual CSS files) and then per-page tweaks for things that can't be reused.
From what I've found, this isn't in the spec and is a synapse-ism, and is what elements will use if a path isn't specified. We can use whatever endoint we want.
I noticed #801 was merged, so I rebased on it and am trying to use askama templates for login and consent webpages. There's a serious issue here : oxide-auth-axum is implemented as a synchronous backend, and porting it to async is non-trivial (see an old MR at their site - that's 1500 lines of code to port oxide-auth-actix to async). But askama is async, and the consent action needs its output in a sync function.
So I'm stuck here right now. I could try and implement that async port in the future, but that will probably take quite some time - and I'm going to have less free time in the weeks to come.
Besides, my main motivation to do this is to support external OIDC providers in the end. But I started thinking that such a feature would have no code in common with this MR.
So the alternative is either more work for the "act as OIDC provider" feature, or start another MR to implement OIDC delegation straight away.
Any opinions on that ?
For the record, someone already implemented async in oxide-auth with axum : https://github.com/mtelahun/axum-oauth
From what I understand, and what I can see in that PR, there is nothing in oxide-auth itself that needs to be changed to be asynchronous as there is no network or disk interaction within the library. That PR adds an example of how you can use asynchronous frameworks with the library.
askama, in contrast, is completely synchronous within its templates. To use asynchronous code you have to run it before rendering the templates and pass it into the template.
Regarding OIDC, this PR from what I understand is a necessary step, and third party OIDC will build upon it. It would be an addition of a new authentication method to the HTML login page. Although this has nothing in common with legacy OIDC (implemented as a different matrix-native authentication flow), that is depreciated and will stop working in some clients soon.
Don't worry about timelines, you're under absolutely no pressure here. I personally am pretty busy with exams right now!
Aha, I thought askama was asynchronous because I had introduced an async relay function, so I freaked out. Indeed, with your advice I succeeded in returning templates in oxide-auth's authentication flow.
The problem that I have now is that the returned login form is not clickable because of CSP policies. And I couldn't find a way to add headers to
OAuthResponse
s. ATM I'm looking into implementing anOwnerSolicitor
that would accept custom headers. It's a bit of a rabbit hole, but hopefully I'll manage something out of it.7f531034bf
to9a52b6cf9e
Good work! I'm excited to see where this goes!
I assume you're running into issues with the default CSP. You'll need to set a CSP for the page using something like https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/src/web/mod.rs#L32 rather than just directly returning the string.
Form actions don't support nonces (https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/form-action) so you'll probably want
form-action 'self'
In addition to the policy on the index page.9a52b6cf9e
toa78887d979
I had to implement a custom OidcResponse that has CSP header capabilities. In the process I ditched the dependency on oxide-auth-axum. The MR now ships with askama templates support !
The result builds and passes areweoidcyet.com's implementation guide. Docstrings also have been rewritten with a newcomer's standpoint in mind.
a78887d979
toeef6e60826
@ -0,0 +13,4 @@
/// A Web response that can be processed by the OIDC authentication flow before
/// being sent over.
#[derive(Default, Clone, Debug)]
pub struct OidcResponse {
I'm not positive that
OidcResponse
andOidcRequest
should live insrc/web
- I first thought they belonged insrc/api/client/oidc
but that results in a cyclic dependency because that API depends onsrc/web/oidc
itself.Maybe could they be buried deeper into the structure ?
I think you're right that they belong in API, or at least with the OIDC code. There shouldn't be a cyclic dependency as web doesn't depend on API or router.
If you carry on struggling, you could move the OIDC stuff into its own crate.
eef6e60826
to0c4917874e
I haven't reviewed this for spec yet, just code quality. Ran into a bug I couldn't easily fix before I finished going through 'are we oidc yet'.
Ran clippy,
cargo +nightly fmt
andtypos
over it, which created the big diff.OidcRequest/Response should be located in the same crate as use of oxide-auth. It seems like dependencies have been added in many places to support code being spread out - should be avoided if possible.
Ignoring the templates themselves as I'm assuming they're placeholder for now.
@ -58,6 +58,10 @@
#
#port = 8008
# This item is undocumented. Please contribute documentation for it.
Shouldn't be here. Check to see what
well_known
doesfixed in
97b66d2f3e
@ -0,0 +77,4 @@
/// Authorize the device based on the user's consent. If the user allows
/// it to access their data, the client may request a token at the
/// [super::token::token] endpoint.
pub(crate) async fn authorize_consent(
Testing going through https://areweoidcyet.com/ with the server running with the following config:
When we get to this point, we get a crash as this seems to return an empty body
Ahh I get it - if you set
it succeeds, right ? Then it's solved in
2baeba41
Nope, not solved. I fixed the issue with IP literals in particular in
7ff9d770e3
, this is a different issueOK, obviously I was too quick using
OidcResponse
inapi/client/oidc/discovery.rs
. This specific issue should be fixed with-
e09d60a6df
- - discarded by357c2335
.I'm a bit jealous of your runtime logs, I only have access to a release binary, because I don't have enough disk space to compile debug builds. If I had access to that when debugging oxide-auth at the beggining, I'd have saved quite some hours !
Actually,
OidcResponse
needs a deeper rewrite. I thought I had copied over all functionalities from oxide-auth-axum'sOAuthResponse
, but it appears I copied the wrong implementation. I'm on it now. BTW my machine built a debug build this night, thanks tonix --store ...
and the workaround I published in #822Done in
357c2335
- rewrite ofOidcResponse
'sIntoResponse
implementation. Should fix the issue at hand (the location header was not set), and a bunch of others.Can you confirm this build passes areweoidcyet.com's test page ?
@ -0,0 +34,4 @@
http::StatusCode::NOT_FOUND,
ClientErrorBody::Standard {
kind: ClientErrorKind::Unrecognized,
message: "This homeserver doesn't do OIDC authentication.".to_owned(),
'This homeserver has disabled OIDC authentication'
fixed in
f4e0ff9e13
@ -0,0 +71,4 @@
.unwrap(),
),
revocation_endpoint: issuer
.join("_matrix/client/unstable/org.matrix.msc2964/revoke")
Missing starting slash here?
Nice catch ! fixed in
0781453572
@ -117,6 +117,21 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::get_protocols_route)
.route("/_matrix/client/unstable/thirdparty/protocols",
get(client::get_protocols_route_unstable))
// MSC2965 route.
Nitpicking but include the MSC titles please
Right ! fixed in
f4e0ff9e13
@ -274,0 +275,4 @@
if auth.enable_oidc_login && config.well_known.client.is_none() {
return Err!(Config(
"auth.enable_oidc_login",
"Oidc authentication is enabled but the well-known client is not set."
OIDC / HTML URL shouldn't be tied to the client-server API URL. Can use it as a default, but should be a separate config option
I believed that until we implement delegation, the OIDC issuer should run at the same URL as the server. Should that be
config.well_known.server
?And, if we make this a configurable default, then if the admin sets it to some other server, that server will expect all sorts of things like client provisioning. I naïvely thought we could avoid that, and implement delegation over standard OIDC servers in a later MR.
Maybe it's silly - or too ambitious. I don't get why Matrix did that overlay over OIDC. I hope Conduwuit can support standard OIDC issuers like Kanidm. Does that sound realistic ?
So this is not requesting delegation, but for the case where, for example, continuwuity is running on matrix.somedomain.com, but the user wants login pages, etc to appear on somedomain.com (and they've set their reverse proxy up correctly for that). For that to work, they'd need to be able to set the URL to a different value to the C/S URL
In fact, this but might not even be necessary, looks like it defaults to "Continuwuity" in the only place it's used

The well-known client is used in
src/api/client/oidc/discovery.rs
to advertise the OIDC issuer. That's why I check on it at startup - if we are the OIDC issuer, we need an URL to advertise.That should be the well-known server instead, right ?
On the other hand, I wanted the login and consent page to display the current server name as a title. That's what the
hostname
variable is for (that's insrc/api/client/oidc/login.rs
). The well-known client was a hasty fill for the job. Is there a config variable that would be more adapted ? Or should we add eg anadvertised_servername
in[global.auth]
?For displaying the server name, you want
config.server_name
, which is what ends up being a part of the username.For the OIDC Discovery document, client is OK as a default, given that it's usually clients accessing those endpoints. I'll have a more in-depth look at that when I review for spec/functionality.
OK ! Added in
2baeba41
- proxy users would set that variable anyway, right ? So this settles that, doesn't it ?Yep, you have to set
server_name
to start up the server@ -1882,0 +1892,4 @@
/// The URL where the user is able to access the account management
/// capabilities of the homeserver. Only used if `enable_oidc_login` is set.
/// Unset by default.
pub enable_oidc_account_management: bool,
Doc comment doesn't match here. Should have a default set.
I updated it to match the implementation in
97b66d2f3e
- I guess the question to use a set or just a boolean is dependent on our other conversation onsrc/core/config/check.rs
.@ -111,6 +111,7 @@ webpage.workspace = true
webpage.optional = true
blurhash.workspace = true
blurhash.optional = true
oxide-auth.workspace = true
Is there a way we can avoid adding the oxide-auth dependency to so many crates? Reexporting necessary types, maybe
That would need a separate crate as you proposed.
BTW I'm sorry, I can't run
cargo +nightly fmt
on my machine right now, lack of resources here. I'll try to set it up.DW, I'm about to do that + fix some compile errors
@ -0,0 +48,4 @@
pub fn preconfigured() -> Self {
Self {
registrar: Mutex::new(
vec![Client::public(
Before I forget about this old commit: this is purely illustrative, stating how to populate the initial client registry with an app. We should remove that sample client before long.
e09d60a6df
to357c233576
@ -118,2 +118,4 @@
.route("/_matrix/client/unstable/thirdparty/protocols",
get(client::get_protocols_route_unstable))
// MSC2965: OAuth 2.0 Authorization Server Metadata discovery.
.route("/_matrix/client/unstable/org.matrix.msc2965/auth_metadata",
Any idea why Element X doesn't use the OIDC login endpoint ? I thought that maybe they don't use the unstable endpoints URLs anymore - the related MSCs were merged recently. Should I try to remove the unstable prefixes from the routes ?
You can try that, don't forget you can register the same handler on multiple routes.
I did try that (remove the unstable prefix from the OIDC discovery endpoint), the issue is still the same.
I must say I don't know what to do else. I did have some oauth knowledge before starting this MR because I already had implemented a client in react, but I'm definitely no expert in Matrix clients - I just used to run a conduwuit server for my family. This MR already contains all I can contribute at the moment, I'm afraid. Besides, I have plenty of paid work in another field coming up for the next two months, so I can't invest much time in debugging this issue.
If someone comes up with something else to try, I might find a bit of time for that though.
I'll have a look and see what I can figure out, but if I can't get it to work, I guess we can continue without that for now.
Taking a look at my Element X IOS Nightly logs, I see this:
2025-05-31T18:44:13.477754Z DEBUG matrix_sdk::http_client::native: Sending request num_attempt=1 | crates/matrix-sdk/src/http_client/native.rs:78 | spans: send{request_id="REQ-3" method=GET uri="https://matrix-client.matrix.org/_matrix/client/unstable/org.matrix.msc2965/auth_metadata"}
I'll see if I can figure out why it's not making the request here
9c7c7e7798
tobb9e8af4e0
@ -0,0 +9,4 @@
pub redirect_uri: Url,
pub scope: String,
pub state: String,
pub code_challenge: String,
Now that clients are confirmed to be connecting correctly (confirmed in
#development:continuwuity.org
, I got mxtoken and Element X iOS to correctly detect and start using OIDC), there's an issue with the initial redirect to/authorize
:Failed to deserialize query string: missing field code_challenge
In the initial request, this, and other fields are not provided, but required by this struct (and similar ones like LoginQuery).
The only five query params provided at the start of the flow are
client_id
,response_type
,redirect_uri
,state
, andscope
. The rest are seemingly used later on?Both
code_challenge
andcode_challenge_method
are necessary for PKCE, which is required as per the MSC on OIDC login. If a client is truly MSC2964 compatible, those parameters must be sent.The same MSC also appears to suggest the
response_mode
parameter is required (it is explicitly mentioned multiple times without a default, and has the client check the server for compatibility with different modes), so it should also be sent every time. Which client wasn't sending these?I'm going to spend some time on this over the next week to see if we can get it working. It doesn't seem far off being finished.
Great ! I'm totally busy until the 8th of june, so I won't be able to help this week, but I'll try to follow your advances remotely - indeed, I think the MR is pretty close to the needed functionality, and I'm confident you'll find out how to make it work.
I guess the code will need some cleanup once it works, I think I'll focus on that when I'm back.
Thanks @nex
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.