The normal authentication process for OIDC is
web-based and involves a series of HTTP redirects, interspersed
with web pages that you interact with. Something that wants to
authenticate you will redirect you to the OIDC identity server's
website, which will ask you for your login and password and maybe
MFA authentication, check them, and then HTTP redirect you back to
a 'callback' or 'redirect' URL that will transfer a magic code from
the OIDC server to the OIDC client (generally as a URL query
parameter). All of this happens in your browser, which means that
the OIDC client and server don't need to be able to directly talk
to each other, allowing you to use an external cloud/SaaS OIDC IdP
to authenticate to a high-security internal website that isn't
reachable from the outside world and maybe isn't allowed to make
random outgoing HTTP connections.
(The magic code transferred in the final HTTP redirect is apparently
often not the authentication token itself but instead something the
client can use for a short time to obtain the real authentication
token. This does require the client to be able to make an outgoing
HTTP connection, which is usually okay.)
When the OIDC client initiates the HTTP redirection to the OIDC IdP
server, one of the parameters it passes along is the 'redirect uri'
it wants the OIDC server to use to pass the magic code back to it.
A malicious client (or something that's gotten a client's ID and
secret) could do some mischief by manipulating this redirect URL,
so the standard specifically requires that OIDC IdP have a list
of allowed redirect uris for each registered client. The
standard also says that in theory, the client's provided redirect
uri and the configured redirect uris are compared as literal
string values.
So, for example, 'https://example.org/callback' doesn't match
'https://example.org/callback/'.
This is straightforward when it comes to websites as OIDC clients,
since they should have well defined callback urls that you can
configure directly into your OIDC IdP when you set up each of them.
It gets more hairy when what you're dealing with is programs as
OIDC clients, where they are (for example) trying to get an OIDC
token so they can authenticate to your IMAP server with OAuth2, since these programs don't
normally have a website. Historically, there are several approaches
that people have taken for programs (or seem to have, based on my
reading so far).
Very early on in OAuth2's history, people apparently defined the
special redirect uri value 'urn:ietf:wg:oauth:2.0:oob' (which is
now hard to find or identify documentation on). An OAuth2 IdP that
saw this redirect uri (and maybe had it allowed for the client) was
supposed to not redirect you but instead show you a HTML page with
the magic OIDC code displayed on it, so you could copy and paste
the code into your local program. This value is now obsolete but
it may still be accepted by some IdPs (you can find it listed for
Google in mutt_oauth2.py, and
I spotted an OIDC IdP server that handles it).
Another option is that the IdP can provide an actual website that
does the same thing; if you get HTTP redirected to it with a valid
code, it will show you the code on a HTML page and you can copy and
paste it. Based on mutt_oauth2.py again, it appears that
Microsoft may have at one point done this, using
https://login.microsoftonline.com/common/oauth2/nativeclient as
the page. You can do this too with your own IdP (or your own website
in general), although it's not recommended for all sorts of reasons.
The final broad approach is to use 'localhost' as the target host
for the redirect. There are several ways to make this work, and one
of them runs into complications with the IdP's redirect uri handling.
The obvious general approach is for your program to run a little
HTTP server that listens on some port on localhost, and capture the
code when the (local) browser gets the HTTP redirect to localhost
and visits the server. The problem here is that you can't necessarily
listen on port 80, so your redirect uri needs to include the port
you're listening (eg 'http://localhost:7000'), and if your OIDC
IdP is following the standard it must be configured not just with
'http://localhost' as the allowed redirect uri but the specific
port you'll use. Also, because of string matching, if the OIDC IdP
lists 'http://localhost:7000', you can't send 'http://localhost:7000/'
despite them being the same URL.
(And your program has to use 'localhost', not '127.0.0.1' or the
IPv6 loopback address; although the two have the same effect, they're
obviously not string-identical.)
Based on experimental evidence from OIDC/OAuth2 client configurations,
I strongly suspect that some large IdP providers have non-standard,
relaxed handling of 'localhost' redirect uris such that their client
configuration lists 'http://localhost' and the IdP will accept
some random port glued on in the actual redirect uri (or maybe this
behavior has been standardized now). I suspect that the IdPs may
also accept the trailing slash case. Honestly, it's hard to see how
you get out of this if you want to handle real client programs out
in the wild.
(Some OIDC IdP software definitely does the standard compliant
string comparison. The one I know of for sure is SimpleSAMLphp's
OIDC module.
Meanwhile, based on reading the source code, Dex uses a relaxed
matching for localhost in its matching function,
provided that there are no redirect uris register for the client.
Dex also still accepts the urn:ietf:wg:oauth:2.0:oob redirect uri,
so I suspect that there are still uses out there in the field.)
If the program has its own embedded web browser that it's in full
control of, it can do what Thunderbird appears to do (based on
reading its source code). As far as I can tell, Thunderbird doesn't
run a local listening server; instead it intercepts the HTTP
redirection to 'http://localhost' itself. When the IdP sends the
final HTTP redirect to localhost with the code embedded in the URL,
Thunderbird effectively just grabs the code from the redirect URL
in the HTTP reply and never actually issues a HTTP request to the
redirect target.
The final option is to not run a localhost HTTP server and to tell
people running your program that when their browser gives them an
'unable to connect' error at the end of the OIDC authentication
process, they need to go to the URL bar and copy the 'code' query
parameter into the program (or if you're being friendly, let them
copy and paste the entire URL and you extract the code parameter).
This allows your program to use a fixed redirect uri, including
just 'http://localhost', because it doesn't have to be able to
listen on it or on any fixed port.
(This is effectively a more secure but less user friendly version
of the old 'copy a code that the website displayed' OAuth2 approach,
and that approach wasn't all that user friendly to start with.)
PS: An OIDC redirect uri apparently allows things other than http://
and https:// URLs; there is, for example, the 'openid-credential-offer'
scheme.
I believe that the OIDC IdP doesn't particularly do anything with
those redirect uris other than accept them and issue a HTTP redirect
to them with the appropriate code attached. It's up to your local
program or system to intercept HTTP requests for those schemes and
react appropriately, much like Thunderbird does, but perhaps easier
because you can probably register the program as handling all
'whatever-special://' URLs so the redirect is automatically handed
off to it.
(I suspect that there are more complexities in the whole OIDC and
OAuth2 redirect uri area, since I'm new to the whole thing.)