Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1tii8V-001R8o-W6 for pgsql-hackers@arkaria.postgresql.org; Thu, 13 Feb 2025 22:56:49 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.94.2) (envelope-from ) id 1tii8T-00Fhtc-W3 for pgsql-hackers@arkaria.postgresql.org; Thu, 13 Feb 2025 22:56:46 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1tii8T-00FhtT-5G for pgsql-hackers@lists.postgresql.org; Thu, 13 Feb 2025 22:56:46 +0000 Received: from smtp.outgoing.loopia.se ([93.188.3.37]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1tii8Q-000hJD-2M for pgsql-hackers@postgresql.org; Thu, 13 Feb 2025 22:56:45 +0000 Received: from s807.loopia.se (localhost [127.0.0.1]) by s807.loopia.se (Postfix) with ESMTP id 0242929CBB0 for ; Thu, 13 Feb 2025 23:56:41 +0100 (CET) Received: from s934.loopia.se (unknown [172.22.191.5]) by s807.loopia.se (Postfix) with ESMTP id C69F229D093; Thu, 13 Feb 2025 23:56:40 +0100 (CET) Received: from s474.loopia.se (unknown [172.22.191.6]) by s934.loopia.se (Postfix) with ESMTP id C069D7CE913; Thu, 13 Feb 2025 23:56:40 +0100 (CET) X-Virus-Scanned: amavisd-new at amavis.loopia.se X-Spam-Flag: NO X-Spam-Score: -1.2 X-Spam-Level: X-Spam-Status: No, score=-1.2 tagged_above=-999 required=6.2 tests=[ALL_TRUSTED=-1, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1] autolearn=disabled Authentication-Results: s474.loopia.se (amavisd-new); dkim=pass (2048-bit key) header.d=yesql.se Received: from s980.loopia.se ([172.22.191.6]) by s474.loopia.se (s474.loopia.se [172.22.190.14]) (amavisd-new, port 10024) with LMTP id gNvMGoFth5fh; Thu, 13 Feb 2025 23:56:40 +0100 (CET) X-Loopia-Auth: user X-Loopia-User: daniel@yesql.se X-Loopia-Originating-IP: 89.255.232.193 Received: from smtpclient.apple (customer-89-255-232-193.stosn.net [89.255.232.193]) (Authenticated sender: daniel@yesql.se) by s980.loopia.se (Postfix) with ESMTPSA id 85FE622015F2; Thu, 13 Feb 2025 23:56:39 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yesql.se; s=loopiadkim1707475645; t=1739487399; bh=l8PLpRwVo2Jn2+zGIvUh4cOp5rXfvnRGJzaukPLEIXo=; h=From:Subject:Date:In-Reply-To:Cc:To:References; b=BSgicaSHd+cuORmVs/hro17Gworx6znw6eMwDPs7mEx/ZI+L1+O4RfDQ89/H8xB9X zLck1bBg8xX4uoMFVEitCCXgWYvyIK325eG3mXLGl1r2B5tUAYVp/Ff+zZqmUIrMsP jAMcH2BiIh4ZFWIvH1Iw3XY7nK7MTf8Z2P+6Zk39xg3Ug1XNqBm3k4PYifUFBfb9HG zMSk+5cswtXQRXjWbQN8L1nsJYEe8Ja39UOYVvad3Z6StZfsizNgL6R2akvDLe2XmY mFCYnlbkH4DAwUyz8rN9rTOjWuQLsmoNzNynduvK3dVVIJ2/Bv/XHLrujAE1I8dJpl rGd/5BTM8capw== From: Daniel Gustafsson Message-Id: <83C44AB4-24B0-437F-B139-B5CBC5821BB1@yesql.se> Content-Type: multipart/mixed; boundary="Apple-Mail=_43C3B3B1-87EA-4831-9D4F-5C67D4DD918F" Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3776.700.51.11.1\)) Subject: Re: [PoC] Federated Authn/z with OAUTHBEARER Date: Thu, 13 Feb 2025 23:56:29 +0100 In-Reply-To: Cc: Peter Eisentraut , Antonin Houska , PostgreSQL Hackers To: Jacob Champion References: <6bde5f56-9e7a-4148-b81c-eb6532cb3651@eisentraut.org> <9B417838-B872-453D-BBFB-99FA957EB723@yesql.se> <54c008ad-1c23-47d8-ba6c-bed98a39c31c@eisentraut.org> <06102deb-00ef-431d-a3ea-65afb246700a@eisentraut.org> <39A09EC8-2D72-4130-8C3E-8F3D1CC69A12@yesql.se> <55EA9E76-9866-486C-9C7F-C5E8B071C50E@yesql.se> <35CBB6AD-B847-4C26-9A7B-019279F280C8@yesql.se> <9F184CFE-ED8D-4B86-9529-F07821108C93@yesql.se> X-Mailer: Apple Mail (2.3776.700.51.11.1) List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --Apple-Mail=_43C3B3B1-87EA-4831-9D4F-5C67D4DD918F Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=us-ascii > On 8 Feb 2025, at 02:56, Jacob Champion = wrote: Thanks for the new version! > - 0004 gets a missed pgperltidy and explicitly skips unsupported tests > on Windows. +if ($Config{osname} eq 'MSWin32') We can get away with nog importing Config at all since Test::Utils = export a symbol for this already in $windows_os. Fixed in my attached. > Daniel and I talked at FOSDEM about wanting to have additional > guardrails on the server-side validator API. Ideally, we'd wait for > major version boundaries to change APIs, as per usual. But if any bugs > come to light that affect the security of the system, we may want to > have more control over the boundary between the server and the > validator. So I've added two features to the API. I think what you added there is quite sufficient for handling the worst = case that ideally should never happen. Even though I can't see us breaking = this given the code being trivial, I also don't feel like realizing after the = fact when we need it that it was subtly broken, so I added a test validator = which use the wrong ABI version. I have now read the entire patch cover-to-cover twice to try and catch = any rough or sharp edges. Unsurprisingly given the number of revisions this = patch has gone through, and the number of hours that have been put into it, = there isn't much to be found. Most of my findings below are well and truly in = the nit- pickery category (and my favourite, paranoia-induced defensive programming). There are no architectural flaws that I can detect, and = cross- referencing with the RFC's I don't see anything mixed up in spec = compliance. To make it easier for you to see what I mean I have implemented most of = the comments and attached as a fixup patch, from which you can cherry-pick = hunks you agree with. Those I didn't implement should be marked as such = below. As we discussed off-list I took the liberty of squashing the previous = fixup patches into a single one, and squashed your fixes for my comments = against v47 into 0001. All of my proposals are in 0004. Some comments: + The system which hosts the protected resources which are The repetition of "which .. which" reads a bit off to me, I propose to simplify as "The system hosting then.." instead. + The organization, product vendor, or other entity which = develops and/or + administers the OAuth servers and clients for a given = application. Since we define terminology here, shouldn't this be "OAuth resource = servers"? + PostgreSQL does not provide an = authorization + server; it's obtained from the OAuth provider. The "obtained from" part makes it sound like you need to get some server software to run this with PostgreSQL. How about "; it is the = responsibility of.."? + setting. No = variations in + case or format are permitted. While not wrong in any way, I think it would be clearer to write = "formatting" here since that's really what we are talking about no? + Build with libcurl support for OAuth 2.0 client flows. + This requires the curl package to = be + installed. Building with this will check for the required = header files I don't think we need to document that curl need to be installed since = it's likely already on the system of anyone reading this. I do however think = we should state the minimum required version. + setting in the server's HBA = configuration. Nitpickery: I prefer to have the period outside the markup. + the connection will fail. This is required to prevent a class = of "mix-up + attacks" on OAuth clients. Since mix-up attacks aren't very well documented I think we should aid = the readers by linking to the OAuth WG announcement of this class of = attacks. From there readers can find the original paper but I think linking directly = to that is less helpful than the mailinglist post. + running a client via SSH. Client applications may implement their = own flows For consistency with the rest of the docs we should wrap SSH with an tag. + You will then log into your OAuth provider, which will ask whether = you want A think third person here reads more like the rest of the docs. + which libpq will call when when = action is "when when", I think this should really be "when an"? + sprays HTTP traffic (containing several critical secrets) to = standard + error during the OAuth flow This might not be readily understandable by non-native speakers. + Similarly, if you are using Curl inside your application, Should use markup. + more recent versions of Curl that are built to support threadsafe s/threadsafe/thread-safe/g for documentation consistency (ditto in other = places where used as a adjective and not code identifier). + itself; validator modules provide the glue between the server and the = OAuth s/glue/integration layer/ to avoid confusing readers not used to english = idioms. + Since a misbehaving validator might let unauthorized users into the = database, + correct implementation is critical. See "Don't make any bugs" isn't very helpful advice =3D) Expanded on it = slightly. + An OAuth validator module is loaded by dynamically loading one of = the shared The double use of load in "loaded .. loading", rewording to try and = simplify. + The server has ensured that the token is well-formed syntactically, = but no "server" is an overloaded nomenclature here, perhaps using libpq instead = to clearly indicate that it's postgres and not an OAuth server. + The connection will only proceed if the module sets + authorized to true. = To In other places we use the structure name as well and not just the = member, adding that here to be consistent. + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development = Group Off-by-one + * - RFC 7628: https://tools.ietf.org/html/rfc7628 Replacing all tools.ietf.org mentions with datatracker.ietf.org to save = a few redirects. + p++; + if (*p !=3D ',') If the SASL exchange, are we certain that a rogue client cannot inject a message which trips us past the end of string? Should we be doublecheck = when advancing p across the message? +sanitize_char(char c) +{ + static char buf[5]; With the multithreading work on the horizon we should probably avoid = static variables like these to not create work for our future selves? The code = isn't as neat when passing in a buffer/length but it avoids the need for a = static or threadlocal variable. Or am I overthinking this? + initStringInfo(&issuer); + appendStringInfoString(&issuer, ctx->issuer); The double StringInfoData variables in generate_error_response to be = able to JSON escape the issuer is a bit of an eye-sore, a version of escape_json_with_len which also took an offset into the buffer on where = to start maybe? Nothing that is urgent to address now (and I have not = changed anything here), but I'll keep it at the back of my head. + ereport(LOG, errmsg("internal error in OAuth validator = module")); I wonder if this should be using WARNING instead? It's really something = that should trigger alarm bells going off. I've also added an errcode for = easier fleet analysis. In load_validator_library we don't explicitly verify that the required = callback is defined in the returned structure, which seems like a cheap enough = belts and suspenders level check. + if (parsed < 1) + return actx->debugging ? 0 : 1; Is 1 second a sane lower bound on interval for all situations? I'm = starting to wonder if we should be more conservative here, or even make it = configurable in some way? The default if not set of 5 seconds is quite a lot higher than = 1. + if (INT_MAX <=3D parsed) I think it's closer to project to style to keep the variable on the left = side in such comparisons, so changed these. + parsed =3D parse_json_number(expires_in_str); + parsed =3D round(parsed); Shouldn't we floor() the value here to ensure we never report an = expiration time longer than the actual expiration? + * Some services (Google, Azure) spell verification_uri = differently. I did another round of documentation reading and couldn't find any = provider which also use "verification_url_complete". However, since it looks so = similar to verification_url it seems worthwhile to add a comment to save readers = from the same rabbithole. register_socket() doesn't have an error catch for the case when neither = epoll nor kqeue is supported. Shouldn't it set actx_error() here as well? = (Not done in my review patch.) + if (actx->curl_err[0]) + { + size_t len; + + appendPQExpBuffer(&conn->errorMessage, " (%s)", = actx->curl_err); Should this also qualify that the error comes from outside of postgres? Something like "(libcurl:%s)" to match? I haven't changed this in the = attached since I'm still on the fence, but I'm leanings that we probably should. Thoughts? - * We only support one mechanism at the moment, so rather than deal = with a + * We only support two mechanisms at the moment, so rather than deal = with a While there's nothing incorrect about this comment, I have a feeling we = won't support more mechanisms than we can justify having a simple array for = anytime soon =3D) Sorry for the wall of text. In general, I feel that this is getting very close to its final form wrt = being a committable patch, and assuming we don't find anything structurally = unsound in the coming days I don't see a blocker for getting this into v18 = before the final commitfest. If anyone disagrees with this I'd love for that be = brought up. -- Daniel Gustafsson --Apple-Mail=_43C3B3B1-87EA-4831-9D4F-5C67D4DD918F Content-Disposition: attachment; filename=v49-0001-Add-OAUTHBEARER-SASL-mechanism.patch Content-Type: application/octet-stream; x-unix-mode=0644; name="v49-0001-Add-OAUTHBEARER-SASL-mechanism.patch" Content-Transfer-Encoding: quoted-printable =46rom=20187890fbc3dadf0fed125dce7d164e245c3bb7c7=20Mon=20Sep=2017=20= 00:00:00=202001=0AFrom:=20Jacob=20Champion=20= =0ADate:=20Wed,=2023=20Oct=202024=20= 09:37:33=20-0700=0ASubject:=20[PATCH=20v49=201/4]=20Add=20OAUTHBEARER=20= SASL=20mechanism=0A=0ADO=20NOT=20USE=20THIS=20PROOF=20OF=20CONCEPT=20IN=20= PRODUCTION.=0A=0AImplement=20OAUTHBEARER=20(RFC=207628)=20and=20OAuth=20= 2.0=20Device=20Authorization=0AGrants=20(RFC=208628).=20This=20adds=20a=20= new=20auth=20method,=20oauth,=20to=20pg_hba.=20When=0Aspeaking=20to=20a=20= OAuth-enabled=20server,=20it=20looks=20a=20bit=20like=20this:=0A=0A=20=20= =20=20$=20psql=20'host=3Dexample.org=20oauth_issuer=3D...=20= oauth_client_id=3D...'=0A=20=20=20=20Visit=20= https://oauth.example.org/login=20and=20enter=20the=20code:=20FPQ2-M4BG=0A= =0AThe=20OAuth=20issuer=20must=20support=20device=20authorization.=20No=20= other=20OAuth=20flows=0Aare=20currently=20implemented=20(but=20clients=20= may=20provide=20their=20own=20flows).=0A=0AThe=20client=20implementation=20= requires=20libcurl=20and=20its=20development=20headers.=0APass=20= --with-libcurl/-Dlibcurl=3Denabled=20during=20configuration.=20The=20= server=0Aimplementation=20does=20not=20require=20additional=20build-time=20= dependencies,=20but=0Aan=20external=20validator=20module=20must=20be=20= supplied.=0A=0AThomas=20Munro=20wrote=20the=20kqueue()=20implementation=20= for=20oauth-curl;=20thanks!=0A=0ASeveral=20TODOs:=0A-=20perform=20= several=20sanity=20checks=20on=20the=20OAuth=20issuer's=20responses=0A-=20= improve=20error=20debuggability=20during=20the=20OAuth=20handshake=0A-=20= fix=20libcurl=20initialization=20thread-safety=0A-=20harden=20the=20= libcurl=20flow=20implementation=0A-=20fill=20in=20documentation=20stubs=0A= -=20support=20protocol=20"variants"=20implemented=20by=20major=20= providers=0A-=20implement=20more=20helpful=20handling=20of=20HBA=20= misconfigurations=0A-=20use=20logdetail=20during=20auth=20failures=0A-=20= ...and=20more.=0A=0ACo-authored-by:=20Daniel=20Gustafsson=20= =0A---=0A=20.cirrus.tasks.yml=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20=2015=20= +-=0A=20config/programs.m4=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20|=20=20=2065=20+=0A=20configure=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20|=20=20332=20++=0A=20configure.ac=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20|=20=20=2041=20+=0A=20doc/src/sgml/client-auth.sgml=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20252=20++=0A=20= doc/src/sgml/config.sgml=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20|=20=20=2026=20+=0A=20doc/src/sgml/filelist.sgml=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20=20=201=20+=0A=20= doc/src/sgml/installation.sgml=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20|=20=20=2027=20+=0A=20doc/src/sgml/libpq.sgml=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20406=20+++=0A=20= doc/src/sgml/oauth-validators.sgml=20=20=20=20=20=20=20=20=20=20=20=20|=20= =20402=20+++=0A=20doc/src/sgml/postgres.sgml=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20|=20=20=20=201=20+=0A=20= doc/src/sgml/protocol.sgml=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20|=20=20133=20+-=0A=20doc/src/sgml/regress.sgml=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20=2010=20+=0A=20= meson.build=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20100=20+=0A=20= meson_options.txt=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20|=20=20=20=203=20+=0A=20= src/Makefile.global.in=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20|=20=20=20=201=20+=0A=20src/backend/libpq/Makefile=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20=20=201=20= +=0A=20src/backend/libpq/auth-oauth.c=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20|=20=20864=20+++++=0A=20src/backend/libpq/auth.c=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20=2010=20+-=0A=20= src/backend/libpq/hba.c=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20|=20=20=2064=20+-=0A=20src/backend/libpq/meson.build=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20=20=201=20+=0A=20= src/backend/libpq/pg_hba.conf.sample=20=20=20=20=20=20=20=20=20=20|=20=20= =20=204=20+-=0A=20src/backend/utils/adt/hbafuncs.c=20=20=20=20=20=20=20=20= =20=20=20=20=20=20|=20=20=2019=20+=0A=20= src/backend/utils/misc/guc_tables.c=20=20=20=20=20=20=20=20=20=20=20|=20=20= =2012=20+=0A=20src/backend/utils/misc/postgresql.conf.sample=20|=20=20=20= =203=20+=0A=20src/include/common/oauth-common.h=20=20=20=20=20=20=20=20=20= =20=20=20=20|=20=20=2019=20+=0A=20src/include/libpq/auth.h=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20=20=201=20+=0A=20= src/include/libpq/hba.h=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20|=20=20=20=207=20+-=0A=20src/include/libpq/oauth.h=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20=2054=20= +=0A=20src/include/pg_config.h.in=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20|=20=20=20=209=20+=0A=20src/interfaces/libpq/Makefile=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20=2011=20+-=0A=20= src/interfaces/libpq/exports.txt=20=20=20=20=20=20=20=20=20=20=20=20=20=20= |=20=20=20=203=20+=0A=20src/interfaces/libpq/fe-auth-oauth-curl.c=20=20=20= =20=20|=202858=20+++++++++++++++++=0A=20= src/interfaces/libpq/fe-auth-oauth.c=20=20=20=20=20=20=20=20=20=20|=20= 1153=20+++++++=0A=20src/interfaces/libpq/fe-auth-oauth.h=20=20=20=20=20=20= =20=20=20=20|=20=20=2045=20+=0A=20src/interfaces/libpq/fe-auth.c=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20=2036=20+-=0A=20= src/interfaces/libpq/fe-auth.h=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20|=20=20=20=203=20+=0A=20src/interfaces/libpq/fe-connect.c=20=20=20=20=20= =20=20=20=20=20=20=20=20|=20=20=2048=20+-=0A=20= src/interfaces/libpq/libpq-fe.h=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20|=20=20=2085=20+=0A=20src/interfaces/libpq/libpq-int.h=20=20=20=20=20=20= =20=20=20=20=20=20=20=20|=20=20=2013=20+-=0A=20= src/interfaces/libpq/meson.build=20=20=20=20=20=20=20=20=20=20=20=20=20=20= |=20=20=20=205=20+=0A=20src/makefiles/meson.build=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20|=20=20=20=201=20+=0A=20= src/test/authentication/t/001_password.pl=20=20=20=20=20|=20=20=20=208=20= +-=0A=20src/test/modules/Makefile=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20|=20=20=20=201=20+=0A=20= src/test/modules/meson.build=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20|=20=20=20=201=20+=0A=20= src/test/modules/oauth_validator/.gitignore=20=20=20|=20=20=20=204=20+=0A= =20src/test/modules/oauth_validator/Makefile=20=20=20=20=20|=20=20=2040=20= +=0A=20src/test/modules/oauth_validator/README=20=20=20=20=20=20=20|=20=20= =2013=20+=0A=20.../modules/oauth_validator/fail_validator.c=20=20|=20=20=20= 42=20+=0A=20src/test/modules/oauth_validator/meson.build=20=20|=20=20=20= 69=20+=0A=20.../oauth_validator/oauth_hook_client.c=20=20=20=20=20=20=20= |=20=20293=20++=0A=20.../modules/oauth_validator/t/001_server.pl=20=20=20= |=20=20566=20++++=0A=20.../modules/oauth_validator/t/002_client.pl=20=20=20= |=20=20154=20+=0A=20.../modules/oauth_validator/t/OAuth/Server.pm=20|=20=20= 140=20+=0A=20.../modules/oauth_validator/t/oauth_server.py=20|=20=20391=20= +++=0A=20src/test/modules/oauth_validator/validator.c=20=20|=20=20135=20= +=0A=20src/test/perl/PostgreSQL/Test/Cluster.pm=20=20=20=20=20=20|=20=20=20= 20=20+-=0A=20src/tools/pgindent/pgindent=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20|=20=20=2014=20+=0A=20= src/tools/pgindent/typedefs.list=20=20=20=20=20=20=20=20=20=20=20=20=20=20= |=20=20=2011=20+=0A=2059=20files=20changed,=209007=20insertions(+),=2039=20= deletions(-)=0A=20create=20mode=20100644=20= doc/src/sgml/oauth-validators.sgml=0A=20create=20mode=20100644=20= src/backend/libpq/auth-oauth.c=0A=20create=20mode=20100644=20= src/include/common/oauth-common.h=0A=20create=20mode=20100644=20= src/include/libpq/oauth.h=0A=20create=20mode=20100644=20= src/interfaces/libpq/fe-auth-oauth-curl.c=0A=20create=20mode=20100644=20= src/interfaces/libpq/fe-auth-oauth.c=0A=20create=20mode=20100644=20= src/interfaces/libpq/fe-auth-oauth.h=0A=20create=20mode=20100644=20= src/test/modules/oauth_validator/.gitignore=0A=20create=20mode=20100644=20= src/test/modules/oauth_validator/Makefile=0A=20create=20mode=20100644=20= src/test/modules/oauth_validator/README=0A=20create=20mode=20100644=20= src/test/modules/oauth_validator/fail_validator.c=0A=20create=20mode=20= 100644=20src/test/modules/oauth_validator/meson.build=0A=20create=20mode=20= 100644=20src/test/modules/oauth_validator/oauth_hook_client.c=0A=20= create=20mode=20100644=20= src/test/modules/oauth_validator/t/001_server.pl=0A=20create=20mode=20= 100644=20src/test/modules/oauth_validator/t/002_client.pl=0A=20create=20= mode=20100644=20src/test/modules/oauth_validator/t/OAuth/Server.pm=0A=20= create=20mode=20100755=20= src/test/modules/oauth_validator/t/oauth_server.py=0A=20create=20mode=20= 100644=20src/test/modules/oauth_validator/validator.c=0A=0Adiff=20--git=20= a/.cirrus.tasks.yml=20b/.cirrus.tasks.yml=0Aindex=20= fffa438cec1..2f5f5ef21a8=20100644=0A---=20a/.cirrus.tasks.yml=0A+++=20= b/.cirrus.tasks.yml=0A@@=20-23,7=20+23,7=20@@=20env:=0A=20=20=20= MTEST_ARGS:=20--print-errorlogs=20--no-rebuild=20-C=20build=0A=20=20=20= PGCTLTIMEOUT:=20120=20#=20avoids=20spurious=20failures=20during=20= parallel=20tests=0A=20=20=20TEMP_CONFIG:=20= ${CIRRUS_WORKING_DIR}/src/tools/ci/pg_ci_base.conf=0A-=20=20= PG_TEST_EXTRA:=20kerberos=20ldap=20ssl=20libpq_encryption=20load_balance=0A= +=20=20PG_TEST_EXTRA:=20kerberos=20ldap=20ssl=20libpq_encryption=20= load_balance=20oauth=0A=20=0A=20=0A=20#=20What=20files=20to=20preserve=20= in=20case=20tests=20fail=0A@@=20-167,7=20+167,7=20@@=20task:=0A=20=20=20=20= =20chown=20root:postgres=20/tmp/cores=0A=20=20=20=20=20sysctl=20= kern.corefile=3D'/tmp/cores/%N.%P.core'=0A=20=20=20= setup_additional_packages_script:=20|=0A-=20=20=20=20#pkg=20install=20-y=20= ...=0A+=20=20=20=20pkg=20install=20-y=20curl=0A=20=0A=20=20=20#=20NB:=20= Intentionally=20build=20without=20-Dllvm.=20The=20freebsd=20image=20size=20= is=20already=0A=20=20=20#=20large=20enough=20to=20make=20VM=20startup=20= slow,=20and=20even=20without=20llvm=20freebsd=0A@@=20-329,6=20+329,7=20= @@=20LINUX_CONFIGURE_FEATURES:=20&LINUX_CONFIGURE_FEATURES=20>-=0A=20=20=20= --with-gssapi=0A=20=20=20--with-icu=0A=20=20=20--with-ldap=0A+=20=20= --with-libcurl=0A=20=20=20--with-libxml=0A=20=20=20--with-libxslt=0A=20=20= =20--with-llvm=0A@@=20-422,8=20+423,10=20@@=20task:=0A=20=20=20=20=20EOF=0A= =20=0A=20=20=20setup_additional_packages_script:=20|=0A-=20=20=20=20= #apt-get=20update=0A-=20=20=20=20#DEBIAN_FRONTEND=3Dnoninteractive=20= apt-get=20-y=20install=20...=0A+=20=20=20=20apt-get=20update=0A+=20=20=20= =20DEBIAN_FRONTEND=3Dnoninteractive=20apt-get=20-y=20install=20\=0A+=20=20= =20=20=20=20libcurl4-openssl-dev=20\=0A+=20=20=20=20=20=20= libcurl4-openssl-dev:i386=20\=0A=20=0A=20=20=20matrix:=0A=20=20=20=20=20= -=20name:=20Linux=20-=20Debian=20Bookworm=20-=20Autoconf=0A@@=20-799,8=20= +802,8=20@@=20task:=0A=20=20=20=20=20folder:=20$CCACHE_DIR=0A=20=0A=20=20= =20setup_additional_packages_script:=20|=0A-=20=20=20=20#apt-get=20= update=0A-=20=20=20=20#DEBIAN_FRONTEND=3Dnoninteractive=20apt-get=20-y=20= install=20...=0A+=20=20=20=20apt-get=20update=0A+=20=20=20=20= DEBIAN_FRONTEND=3Dnoninteractive=20apt-get=20-y=20install=20= libcurl4-openssl-dev=0A=20=0A=20=20=20###=0A=20=20=20#=20Test=20that=20= code=20can=20be=20built=20with=20gcc/clang=20without=20warnings=0Adiff=20= --git=20a/config/programs.m4=20b/config/programs.m4=0Aindex=20= 7b55c2664a6..ead427046f5=20100644=0A---=20a/config/programs.m4=0A+++=20= b/config/programs.m4=0A@@=20-274,3=20+274,68=20@@=20= AC_DEFUN([PGAC_CHECK_STRIP],=0A=20=20=20AC_SUBST(STRIP_STATIC_LIB)=0A=20=20= =20AC_SUBST(STRIP_SHARED_LIB)=0A=20])#=20PGAC_CHECK_STRIP=0A+=0A+=0A+=0A= +#=20PGAC_CHECK_LIBCURL=0A+#=20------------------=0A+#=20Check=20for=20= required=20libraries=20and=20headers,=20and=20test=20to=20see=20whether=20= the=20current=0A+#=20installation=20of=20libcurl=20is=20threadsafe.=0A+=0A= +AC_DEFUN([PGAC_CHECK_LIBCURL],=0A+[=0A+=20=20= AC_CHECK_HEADER(curl/curl.h,=20[],=0A+=09=09=09=09=20=20= [AC_MSG_ERROR([header=20file=20=20is=20required=20for=20= --with-libcurl])])=0A+=20=20AC_CHECK_LIB(curl,=20curl_multi_init,=20[],=0A= +=09=09=09=20=20=20[AC_MSG_ERROR([library=20'curl'=20does=20not=20= provide=20curl_multi_init])])=0A+=0A+=20=20#=20Check=20to=20see=20= whether=20the=20current=20platform=20supports=20threadsafe=20Curl=0A+=20=20= #=20initialization.=0A+=20=20AC_CACHE_CHECK([for=20curl_global_init=20= thread=20safety],=20[pgac_cv__libcurl_threadsafe_init],=0A+=20=20= [AC_RUN_IFELSE([AC_LANG_PROGRAM([=0A+#include=20=0A+],[=0A+=20= =20=20=20curl_version_info_data=20*info;=0A+=0A+=20=20=20=20if=20= (curl_global_init(CURL_GLOBAL_ALL))=0A+=20=20=20=20=20=20=20=20return=20= -1;=0A+=0A+=20=20=20=20info=20=3D=20curl_version_info(CURLVERSION_NOW);=0A= +#ifdef=20CURL_VERSION_THREADSAFE=0A+=20=20=20=20if=20(info->features=20= &=20CURL_VERSION_THREADSAFE)=0A+=20=20=20=20=20=20=20=20return=200;=0A= +#endif=0A+=0A+=20=20=20=20return=201;=0A+])],=0A+=20=20= [pgac_cv__libcurl_threadsafe_init=3Dyes],=0A+=20=20= [pgac_cv__libcurl_threadsafe_init=3Dno],=0A+=20=20= [pgac_cv__libcurl_threadsafe_init=3Dunknown])])=0A+=20=20if=20test=20= x"$pgac_cv__libcurl_threadsafe_init"=20=3D=20xyes=20;=20then=0A+=20=20=20= =20AC_DEFINE(HAVE_THREADSAFE_CURL_GLOBAL_INIT,=201,=0A+=20=20=20=20=20=20= =20=20=20=20=20=20=20=20[Define=20to=201=20if=20curl_global_init()=20is=20= guaranteed=20to=20be=20threadsafe.])=0A+=20=20fi=0A+=0A+=20=20#=20Warn=20= if=20a=20thread-friendly=20DNS=20resolver=20isn't=20built.=0A+=20=20= AC_CACHE_CHECK([for=20curl=20support=20for=20asynchronous=20DNS],=20= [pgac_cv__libcurl_async_dns],=0A+=20=20[AC_RUN_IFELSE([AC_LANG_PROGRAM([=0A= +#include=20=0A+],[=0A+=20=20=20=20curl_version_info_data=20= *info;=0A+=0A+=20=20=20=20if=20(curl_global_init(CURL_GLOBAL_ALL))=0A+=20= =20=20=20=20=20=20=20return=20-1;=0A+=0A+=20=20=20=20info=20=3D=20= curl_version_info(CURLVERSION_NOW);=0A+=20=20=20=20return=20= (info->features=20&=20CURL_VERSION_ASYNCHDNS)=20?=200=20:=201;=0A+])],=0A= +=20=20[pgac_cv__libcurl_async_dns=3Dyes],=0A+=20=20= [pgac_cv__libcurl_async_dns=3Dno],=0A+=20=20= [pgac_cv__libcurl_async_dns=3Dunknown])])=0A+=20=20if=20test=20= x"$pgac_cv__libcurl_async_dns"=20!=3D=20xyes=20;=20then=0A+=20=20=20=20= AC_MSG_WARN([=0A+***=20The=20installed=20version=20of=20libcurl=20does=20= not=20support=20asynchronous=20DNS=0A+***=20lookups.=20Connection=20= timeouts=20will=20not=20be=20honored=20during=20DNS=20resolution,=0A+***=20= which=20may=20lead=20to=20hangs=20in=20client=20programs.])=0A+=20=20fi=0A= +])#=20PGAC_CHECK_LIBCURL=0Adiff=20--git=20a/configure=20b/configure=0A= index=200ffcaeb4367..93fddd69981=20100755=0A---=20a/configure=0A+++=20= b/configure=0A@@=20-708,6=20+708,9=20@@=20XML2_LIBS=0A=20XML2_CFLAGS=0A=20= XML2_CONFIG=0A=20with_libxml=0A+LIBCURL_LIBS=0A+LIBCURL_CFLAGS=0A= +with_libcurl=0A=20with_uuid=0A=20with_readline=0A=20with_systemd=0A@@=20= -864,6=20+867,7=20@@=20with_readline=0A=20with_libedit_preferred=0A=20= with_uuid=0A=20with_ossp_uuid=0A+with_libcurl=0A=20with_libxml=0A=20= with_libxslt=0A=20with_system_tzdata=0A@@=20-894,6=20+898,8=20@@=20= PKG_CONFIG_PATH=0A=20PKG_CONFIG_LIBDIR=0A=20ICU_CFLAGS=0A=20ICU_LIBS=0A= +LIBCURL_CFLAGS=0A+LIBCURL_LIBS=0A=20XML2_CONFIG=0A=20XML2_CFLAGS=0A=20= XML2_LIBS=0A@@=20-1574,6=20+1580,7=20@@=20Optional=20Packages:=0A=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= prefer=20BSD=20Libedit=20over=20GNU=20Readline=0A=20=20=20= --with-uuid=3DLIB=20=20=20=20=20=20=20=20=20build=20contrib/uuid-ossp=20= using=20LIB=20(bsd,e2fs,ossp)=0A=20=20=20--with-ossp-uuid=20=20=20=20=20=20= =20=20obsolete=20spelling=20of=20--with-uuid=3Dossp=0A+=20=20= --with-libcurl=20=20=20=20=20=20=20=20=20=20build=20with=20libcurl=20= support=0A=20=20=20--with-libxml=20=20=20=20=20=20=20=20=20=20=20build=20= with=20XML=20support=0A=20=20=20--with-libxslt=20=20=20=20=20=20=20=20=20= =20use=20XSLT=20support=20when=20building=20contrib/xml2=0A=20=20=20= --with-system-tzdata=3DDIR=0A@@=20-1607,6=20+1614,10=20@@=20Some=20= influential=20environment=20variables:=0A=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20path=20overriding=20pkg-config's=20built-in=20search=20path=0A= =20=20=20ICU_CFLAGS=20=20C=20compiler=20flags=20for=20ICU,=20overriding=20= pkg-config=0A=20=20=20ICU_LIBS=20=20=20=20linker=20flags=20for=20ICU,=20= overriding=20pkg-config=0A+=20=20LIBCURL_CFLAGS=0A+=20=20=20=20=20=20=20=20= =20=20=20=20=20=20C=20compiler=20flags=20for=20LIBCURL,=20overriding=20= pkg-config=0A+=20=20LIBCURL_LIBS=0A+=20=20=20=20=20=20=20=20=20=20=20=20=20= =20linker=20flags=20for=20LIBCURL,=20overriding=20pkg-config=0A=20=20=20= XML2_CONFIG=20path=20to=20xml2-config=20utility=0A=20=20=20XML2_CFLAGS=20= C=20compiler=20flags=20for=20XML2,=20overriding=20pkg-config=0A=20=20=20= XML2_LIBS=20=20=20linker=20flags=20for=20XML2,=20overriding=20pkg-config=0A= @@=20-8762,6=20+8773,157=20@@=20fi=0A=20=0A=20=0A=20=0A+#=0A+#=20libcurl=0A= +#=0A+{=20$as_echo=20"$as_me:${as_lineno-$LINENO}:=20checking=20whether=20= to=20build=20with=20libcurl=20support"=20>&5=0A+$as_echo_n=20"checking=20= whether=20to=20build=20with=20libcurl=20support...=20"=20>&6;=20}=0A+=0A= +=0A+=0A+#=20Check=20whether=20--with-libcurl=20was=20given.=0A+if=20= test=20"${with_libcurl+set}"=20=3D=20set;=20then=20:=0A+=20=20= withval=3D$with_libcurl;=0A+=20=20case=20$withval=20in=0A+=20=20=20=20= yes)=0A+=0A+$as_echo=20"#define=20USE_LIBCURL=201"=20>>confdefs.h=0A+=0A= +=20=20=20=20=20=20;;=0A+=20=20=20=20no)=0A+=20=20=20=20=20=20:=0A+=20=20= =20=20=20=20;;=0A+=20=20=20=20*)=0A+=20=20=20=20=20=20as_fn_error=20$?=20= "no=20argument=20expected=20for=20--with-libcurl=20option"=20"$LINENO"=20= 5=0A+=20=20=20=20=20=20;;=0A+=20=20esac=0A+=0A+else=0A+=20=20= with_libcurl=3Dno=0A+=0A+fi=0A+=0A+=0A+{=20$as_echo=20= "$as_me:${as_lineno-$LINENO}:=20result:=20$with_libcurl"=20>&5=0A= +$as_echo=20"$with_libcurl"=20>&6;=20}=0A+=0A+=0A+if=20test=20= "$with_libcurl"=20=3D=20yes=20;=20then=0A+=20=20#=20Check=20for=20= libcurl=207.61.0=20or=20higher=20(corresponding=20to=20RHEL8=20and=20the=20= ability=0A+=20=20#=20to=20explicitly=20set=20TLS=201.3=20ciphersuites).=0A= +=0A+pkg_failed=3Dno=0A+{=20$as_echo=20"$as_me:${as_lineno-$LINENO}:=20= checking=20for=20libcurl=20>=3D=207.61.0"=20>&5=0A+$as_echo_n=20= "checking=20for=20libcurl=20>=3D=207.61.0...=20"=20>&6;=20}=0A+=0A+if=20= test=20-n=20"$LIBCURL_CFLAGS";=20then=0A+=20=20=20=20= pkg_cv_LIBCURL_CFLAGS=3D"$LIBCURL_CFLAGS"=0A+=20elif=20test=20-n=20= "$PKG_CONFIG";=20then=0A+=20=20=20=20if=20test=20-n=20"$PKG_CONFIG"=20&&=20= \=0A+=20=20=20=20{=20{=20$as_echo=20"$as_me:${as_lineno-$LINENO}:=20= \$PKG_CONFIG=20--exists=20--print-errors=20\"libcurl=20>=3D=207.61.0\"";=20= }=20>&5=0A+=20=20($PKG_CONFIG=20--exists=20--print-errors=20"libcurl=20= >=3D=207.61.0")=202>&5=0A+=20=20ac_status=3D$?=0A+=20=20$as_echo=20= "$as_me:${as_lineno-$LINENO}:=20\$?=20=3D=20$ac_status"=20>&5=0A+=20=20= test=20$ac_status=20=3D=200;=20};=20then=0A+=20=20= pkg_cv_LIBCURL_CFLAGS=3D`$PKG_CONFIG=20--cflags=20"libcurl=20>=3D=20= 7.61.0"=202>/dev/null`=0A+=09=09=20=20=20=20=20=20test=20"x$?"=20!=3D=20= "x0"=20&&=20pkg_failed=3Dyes=0A+else=0A+=20=20pkg_failed=3Dyes=0A+fi=0A+=20= else=0A+=20=20=20=20pkg_failed=3Duntried=0A+fi=0A+if=20test=20-n=20= "$LIBCURL_LIBS";=20then=0A+=20=20=20=20= pkg_cv_LIBCURL_LIBS=3D"$LIBCURL_LIBS"=0A+=20elif=20test=20-n=20= "$PKG_CONFIG";=20then=0A+=20=20=20=20if=20test=20-n=20"$PKG_CONFIG"=20&&=20= \=0A+=20=20=20=20{=20{=20$as_echo=20"$as_me:${as_lineno-$LINENO}:=20= \$PKG_CONFIG=20--exists=20--print-errors=20\"libcurl=20>=3D=207.61.0\"";=20= }=20>&5=0A+=20=20($PKG_CONFIG=20--exists=20--print-errors=20"libcurl=20= >=3D=207.61.0")=202>&5=0A+=20=20ac_status=3D$?=0A+=20=20$as_echo=20= "$as_me:${as_lineno-$LINENO}:=20\$?=20=3D=20$ac_status"=20>&5=0A+=20=20= test=20$ac_status=20=3D=200;=20};=20then=0A+=20=20= pkg_cv_LIBCURL_LIBS=3D`$PKG_CONFIG=20--libs=20"libcurl=20>=3D=207.61.0"=20= 2>/dev/null`=0A+=09=09=20=20=20=20=20=20test=20"x$?"=20!=3D=20"x0"=20&&=20= pkg_failed=3Dyes=0A+else=0A+=20=20pkg_failed=3Dyes=0A+fi=0A+=20else=0A+=20= =20=20=20pkg_failed=3Duntried=0A+fi=0A+=0A+=0A+=0A+if=20test=20= $pkg_failed=20=3D=20yes;=20then=0A+=20=20=20=20=20=20=20=20{=20$as_echo=20= "$as_me:${as_lineno-$LINENO}:=20result:=20no"=20>&5=0A+$as_echo=20"no"=20= >&6;=20}=0A+=0A+if=20$PKG_CONFIG=20--atleast-pkgconfig-version=200.20;=20= then=0A+=20=20=20=20=20=20=20=20_pkg_short_errors_supported=3Dyes=0A= +else=0A+=20=20=20=20=20=20=20=20_pkg_short_errors_supported=3Dno=0A+fi=0A= +=20=20=20=20=20=20=20=20if=20test=20$_pkg_short_errors_supported=20=3D=20= yes;=20then=0A+=09=20=20=20=20=20=20=20=20= LIBCURL_PKG_ERRORS=3D`$PKG_CONFIG=20--short-errors=20--print-errors=20= --cflags=20--libs=20"libcurl=20>=3D=207.61.0"=202>&1`=0A+=20=20=20=20=20=20= =20=20else=0A+=09=20=20=20=20=20=20=20=20LIBCURL_PKG_ERRORS=3D`$PKG_CONFIG= =20--print-errors=20--cflags=20--libs=20"libcurl=20>=3D=207.61.0"=20= 2>&1`=0A+=20=20=20=20=20=20=20=20fi=0A+=09#=20Put=20the=20nasty=20error=20= message=20in=20config.log=20where=20it=20belongs=0A+=09echo=20= "$LIBCURL_PKG_ERRORS"=20>&5=0A+=0A+=09as_fn_error=20$?=20"Package=20= requirements=20(libcurl=20>=3D=207.61.0)=20were=20not=20met:=0A+=0A= +$LIBCURL_PKG_ERRORS=0A+=0A+Consider=20adjusting=20the=20PKG_CONFIG_PATH=20= environment=20variable=20if=20you=0A+installed=20software=20in=20a=20= non-standard=20prefix.=0A+=0A+Alternatively,=20you=20may=20set=20the=20= environment=20variables=20LIBCURL_CFLAGS=0A+and=20LIBCURL_LIBS=20to=20= avoid=20the=20need=20to=20call=20pkg-config.=0A+See=20the=20pkg-config=20= man=20page=20for=20more=20details."=20"$LINENO"=205=0A+elif=20test=20= $pkg_failed=20=3D=20untried;=20then=0A+=20=20=20=20=20=20=20=20{=20= $as_echo=20"$as_me:${as_lineno-$LINENO}:=20result:=20no"=20>&5=0A= +$as_echo=20"no"=20>&6;=20}=0A+=09{=20{=20$as_echo=20= "$as_me:${as_lineno-$LINENO}:=20error:=20in=20\`$ac_pwd':"=20>&5=0A= +$as_echo=20"$as_me:=20error:=20in=20\`$ac_pwd':"=20>&2;}=0A+as_fn_error=20= $?=20"The=20pkg-config=20script=20could=20not=20be=20found=20or=20is=20= too=20old.=20=20Make=20sure=20it=0A+is=20in=20your=20PATH=20or=20set=20= the=20PKG_CONFIG=20environment=20variable=20to=20the=20full=0A+path=20to=20= pkg-config.=0A+=0A+Alternatively,=20you=20may=20set=20the=20environment=20= variables=20LIBCURL_CFLAGS=0A+and=20LIBCURL_LIBS=20to=20avoid=20the=20= need=20to=20call=20pkg-config.=0A+See=20the=20pkg-config=20man=20page=20= for=20more=20details.=0A+=0A+To=20get=20pkg-config,=20see=20= .=0A+See=20\`config.log'=20for=20= more=20details"=20"$LINENO"=205;=20}=0A+else=0A+=09= LIBCURL_CFLAGS=3D$pkg_cv_LIBCURL_CFLAGS=0A+=09= LIBCURL_LIBS=3D$pkg_cv_LIBCURL_LIBS=0A+=20=20=20=20=20=20=20=20{=20= $as_echo=20"$as_me:${as_lineno-$LINENO}:=20result:=20yes"=20>&5=0A= +$as_echo=20"yes"=20>&6;=20}=0A+=0A+fi=0A+=0A+=20=20#=20We=20only=20care=20= about=20-I,=20-D,=20and=20-L=20switches;=0A+=20=20#=20note=20that=20= -lcurl=20will=20be=20added=20by=20PGAC_CHECK_LIBCURL=20below.=0A+=20=20= for=20pgac_option=20in=20$LIBCURL_CFLAGS;=20do=0A+=20=20=20=20case=20= $pgac_option=20in=0A+=20=20=20=20=20=20-I*|-D*)=20CPPFLAGS=3D"$CPPFLAGS=20= $pgac_option";;=0A+=20=20=20=20esac=0A+=20=20done=0A+=20=20for=20= pgac_option=20in=20$LIBCURL_LIBS;=20do=0A+=20=20=20=20case=20= $pgac_option=20in=0A+=20=20=20=20=20=20-L*)=20LDFLAGS=3D"$LDFLAGS=20= $pgac_option";;=0A+=20=20=20=20esac=0A+=20=20done=0A+=0A+=20=20#=20OAuth=20= requires=20python=20for=20testing=0A+=20=20if=20test=20"$with_python"=20= !=3D=20yes;=20then=0A+=20=20=20=20{=20$as_echo=20= "$as_me:${as_lineno-$LINENO}:=20WARNING:=20***=20OAuth=20support=20tests=20= require=20--with-python=20to=20run"=20>&5=0A+$as_echo=20"$as_me:=20= WARNING:=20***=20OAuth=20support=20tests=20require=20--with-python=20to=20= run"=20>&2;}=0A+=20=20fi=0A+fi=0A+=0A+=0A=20#=0A=20#=20XML=0A=20#=0A@@=20= -12216,6=20+12378,176=20@@=20fi=0A=20=0A=20fi=0A=20=0A+#=20XXX=20libcurl=20= must=20link=20after=20libgssapi_krb5=20on=20FreeBSD=20to=20avoid=20= segfaults=0A+#=20during=20gss_acquire_cred().=20This=20is=20possibly=20= related=20to=20Curl's=20Heimdal=0A+#=20dependency=20on=20that=20= platform?=0A+if=20test=20"$with_libcurl"=20=3D=20yes=20;=20then=0A+=0A+=20= =20ac_fn_c_check_header_mongrel=20"$LINENO"=20"curl/curl.h"=20= "ac_cv_header_curl_curl_h"=20"$ac_includes_default"=0A+if=20test=20= "x$ac_cv_header_curl_curl_h"=20=3D=20xyes;=20then=20:=0A+=0A+else=0A+=20=20= as_fn_error=20$?=20"header=20file=20=20is=20required=20for=20= --with-libcurl"=20"$LINENO"=205=0A+fi=0A+=0A+=0A+=20=20{=20$as_echo=20= "$as_me:${as_lineno-$LINENO}:=20checking=20for=20curl_multi_init=20in=20= -lcurl"=20>&5=0A+$as_echo_n=20"checking=20for=20curl_multi_init=20in=20= -lcurl...=20"=20>&6;=20}=0A+if=20${ac_cv_lib_curl_curl_multi_init+:}=20= false;=20then=20:=0A+=20=20$as_echo_n=20"(cached)=20"=20>&6=0A+else=0A+=20= =20ac_check_lib_save_LIBS=3D$LIBS=0A+LIBS=3D"-lcurl=20=20$LIBS"=0A+cat=20= confdefs.h=20-=20<<_ACEOF=20>conftest.$ac_ext=0A+/*=20end=20confdefs.h.=20= =20*/=0A+=0A+/*=20Override=20any=20GCC=20internal=20prototype=20to=20= avoid=20an=20error.=0A+=20=20=20Use=20char=20because=20int=20might=20= match=20the=20return=20type=20of=20a=20GCC=0A+=20=20=20builtin=20and=20= then=20its=20argument=20prototype=20would=20still=20apply.=20=20*/=0A= +#ifdef=20__cplusplus=0A+extern=20"C"=0A+#endif=0A+char=20= curl_multi_init=20();=0A+int=0A+main=20()=0A+{=0A+return=20= curl_multi_init=20();=0A+=20=20;=0A+=20=20return=200;=0A+}=0A+_ACEOF=0A= +if=20ac_fn_c_try_link=20"$LINENO";=20then=20:=0A+=20=20= ac_cv_lib_curl_curl_multi_init=3Dyes=0A+else=0A+=20=20= ac_cv_lib_curl_curl_multi_init=3Dno=0A+fi=0A+rm=20-f=20core=20= conftest.err=20conftest.$ac_objext=20\=0A+=20=20=20=20conftest$ac_exeext=20= conftest.$ac_ext=0A+LIBS=3D$ac_check_lib_save_LIBS=0A+fi=0A+{=20$as_echo=20= "$as_me:${as_lineno-$LINENO}:=20result:=20= $ac_cv_lib_curl_curl_multi_init"=20>&5=0A+$as_echo=20= "$ac_cv_lib_curl_curl_multi_init"=20>&6;=20}=0A+if=20test=20= "x$ac_cv_lib_curl_curl_multi_init"=20=3D=20xyes;=20then=20:=0A+=20=20cat=20= >>confdefs.h=20<<_ACEOF=0A+#define=20HAVE_LIBCURL=201=0A+_ACEOF=0A+=0A+=20= =20LIBS=3D"-lcurl=20$LIBS"=0A+=0A+else=0A+=20=20as_fn_error=20$?=20= "library=20'curl'=20does=20not=20provide=20curl_multi_init"=20"$LINENO"=20= 5=0A+fi=0A+=0A+=0A+=20=20#=20Check=20to=20see=20whether=20the=20current=20= platform=20supports=20threadsafe=20Curl=0A+=20=20#=20initialization.=0A+=20= =20{=20$as_echo=20"$as_me:${as_lineno-$LINENO}:=20checking=20for=20= curl_global_init=20thread=20safety"=20>&5=0A+$as_echo_n=20"checking=20= for=20curl_global_init=20thread=20safety...=20"=20>&6;=20}=0A+if=20= ${pgac_cv__libcurl_threadsafe_init+:}=20false;=20then=20:=0A+=20=20= $as_echo_n=20"(cached)=20"=20>&6=0A+else=0A+=20=20if=20test=20= "$cross_compiling"=20=3D=20yes;=20then=20:=0A+=20=20= pgac_cv__libcurl_threadsafe_init=3Dunknown=0A+else=0A+=20=20cat=20= confdefs.h=20-=20<<_ACEOF=20>conftest.$ac_ext=0A+/*=20end=20confdefs.h.=20= =20*/=0A+=0A+#include=20=0A+=0A+int=0A+main=20()=0A+{=0A+=0A= +=20=20=20=20curl_version_info_data=20*info;=0A+=0A+=20=20=20=20if=20= (curl_global_init(CURL_GLOBAL_ALL))=0A+=20=20=20=20=20=20=20=20return=20= -1;=0A+=0A+=20=20=20=20info=20=3D=20curl_version_info(CURLVERSION_NOW);=0A= +#ifdef=20CURL_VERSION_THREADSAFE=0A+=20=20=20=20if=20(info->features=20= &=20CURL_VERSION_THREADSAFE)=0A+=20=20=20=20=20=20=20=20return=200;=0A= +#endif=0A+=0A+=20=20=20=20return=201;=0A+=0A+=20=20;=0A+=20=20return=20= 0;=0A+}=0A+_ACEOF=0A+if=20ac_fn_c_try_run=20"$LINENO";=20then=20:=0A+=20=20= pgac_cv__libcurl_threadsafe_init=3Dyes=0A+else=0A+=20=20= pgac_cv__libcurl_threadsafe_init=3Dno=0A+fi=0A+rm=20-f=20core=20*.core=20= core.conftest.*=20gmon.out=20bb.out=20conftest$ac_exeext=20\=0A+=20=20= conftest.$ac_objext=20conftest.beam=20conftest.$ac_ext=0A+fi=0A+=0A+fi=0A= +{=20$as_echo=20"$as_me:${as_lineno-$LINENO}:=20result:=20= $pgac_cv__libcurl_threadsafe_init"=20>&5=0A+$as_echo=20= "$pgac_cv__libcurl_threadsafe_init"=20>&6;=20}=0A+=20=20if=20test=20= x"$pgac_cv__libcurl_threadsafe_init"=20=3D=20xyes=20;=20then=0A+=0A= +$as_echo=20"#define=20HAVE_THREADSAFE_CURL_GLOBAL_INIT=201"=20= >>confdefs.h=0A+=0A+=20=20fi=0A+=0A+=20=20#=20Warn=20if=20a=20= thread-friendly=20DNS=20resolver=20isn't=20built.=0A+=20=20{=20$as_echo=20= "$as_me:${as_lineno-$LINENO}:=20checking=20for=20curl=20support=20for=20= asynchronous=20DNS"=20>&5=0A+$as_echo_n=20"checking=20for=20curl=20= support=20for=20asynchronous=20DNS...=20"=20>&6;=20}=0A+if=20= ${pgac_cv__libcurl_async_dns+:}=20false;=20then=20:=0A+=20=20$as_echo_n=20= "(cached)=20"=20>&6=0A+else=0A+=20=20if=20test=20"$cross_compiling"=20=3D=20= yes;=20then=20:=0A+=20=20pgac_cv__libcurl_async_dns=3Dunknown=0A+else=0A= +=20=20cat=20confdefs.h=20-=20<<_ACEOF=20>conftest.$ac_ext=0A+/*=20end=20= confdefs.h.=20=20*/=0A+=0A+#include=20=0A+=0A+int=0A+main=20= ()=0A+{=0A+=0A+=20=20=20=20curl_version_info_data=20*info;=0A+=0A+=20=20=20= =20if=20(curl_global_init(CURL_GLOBAL_ALL))=0A+=20=20=20=20=20=20=20=20= return=20-1;=0A+=0A+=20=20=20=20info=20=3D=20= curl_version_info(CURLVERSION_NOW);=0A+=20=20=20=20return=20= (info->features=20&=20CURL_VERSION_ASYNCHDNS)=20?=200=20:=201;=0A+=0A+=20= =20;=0A+=20=20return=200;=0A+}=0A+_ACEOF=0A+if=20ac_fn_c_try_run=20= "$LINENO";=20then=20:=0A+=20=20pgac_cv__libcurl_async_dns=3Dyes=0A+else=0A= +=20=20pgac_cv__libcurl_async_dns=3Dno=0A+fi=0A+rm=20-f=20core=20*.core=20= core.conftest.*=20gmon.out=20bb.out=20conftest$ac_exeext=20\=0A+=20=20= conftest.$ac_objext=20conftest.beam=20conftest.$ac_ext=0A+fi=0A+=0A+fi=0A= +{=20$as_echo=20"$as_me:${as_lineno-$LINENO}:=20result:=20= $pgac_cv__libcurl_async_dns"=20>&5=0A+$as_echo=20= "$pgac_cv__libcurl_async_dns"=20>&6;=20}=0A+=20=20if=20test=20= x"$pgac_cv__libcurl_async_dns"=20!=3D=20xyes=20;=20then=0A+=20=20=20=20{=20= $as_echo=20"$as_me:${as_lineno-$LINENO}:=20WARNING:=0A+***=20The=20= installed=20version=20of=20libcurl=20does=20not=20support=20asynchronous=20= DNS=0A+***=20lookups.=20Connection=20timeouts=20will=20not=20be=20= honored=20during=20DNS=20resolution,=0A+***=20which=20may=20lead=20to=20= hangs=20in=20client=20programs."=20>&5=0A+$as_echo=20"$as_me:=20WARNING:=0A= +***=20The=20installed=20version=20of=20libcurl=20does=20not=20support=20= asynchronous=20DNS=0A+***=20lookups.=20Connection=20timeouts=20will=20= not=20be=20honored=20during=20DNS=20resolution,=0A+***=20which=20may=20= lead=20to=20hangs=20in=20client=20programs."=20>&2;}=0A+=20=20fi=0A+=0A= +fi=0A+=0A=20if=20test=20"$with_gssapi"=20=3D=20yes=20;=20then=0A=20=20=20= if=20test=20"$PORTNAME"=20!=3D=20"win32";=20then=0A=20=20=20=20=20{=20= $as_echo=20"$as_me:${as_lineno-$LINENO}:=20checking=20for=20library=20= containing=20gss_store_cred_into"=20>&5=0Adiff=20--git=20a/configure.ac=20= b/configure.ac=0Aindex=20f56681e0d91..b6d02f5ecc7=20100644=0A---=20= a/configure.ac=0A+++=20b/configure.ac=0A@@=20-1007,6=20+1007,40=20@@=20= fi=0A=20AC_SUBST(with_uuid)=0A=20=0A=20=0A+#=0A+#=20libcurl=0A+#=0A= +AC_MSG_CHECKING([whether=20to=20build=20with=20libcurl=20support])=0A= +PGAC_ARG_BOOL(with,=20libcurl,=20no,=20[build=20with=20libcurl=20= support],=0A+=20=20=20=20=20=20=20=20=20=20=20=20=20=20= [AC_DEFINE([USE_LIBCURL],=201,=20[Define=20to=201=20to=20build=20with=20= libcurl=20support.=20(--with-libcurl)])])=0A= +AC_MSG_RESULT([$with_libcurl])=0A+AC_SUBST(with_libcurl)=0A+=0A+if=20= test=20"$with_libcurl"=20=3D=20yes=20;=20then=0A+=20=20#=20Check=20for=20= libcurl=207.61.0=20or=20higher=20(corresponding=20to=20RHEL8=20and=20the=20= ability=0A+=20=20#=20to=20explicitly=20set=20TLS=201.3=20ciphersuites).=0A= +=20=20PKG_CHECK_MODULES(LIBCURL,=20[libcurl=20>=3D=207.61.0])=0A+=0A+=20= =20#=20We=20only=20care=20about=20-I,=20-D,=20and=20-L=20switches;=0A+=20= =20#=20note=20that=20-lcurl=20will=20be=20added=20by=20= PGAC_CHECK_LIBCURL=20below.=0A+=20=20for=20pgac_option=20in=20= $LIBCURL_CFLAGS;=20do=0A+=20=20=20=20case=20$pgac_option=20in=0A+=20=20=20= =20=20=20-I*|-D*)=20CPPFLAGS=3D"$CPPFLAGS=20$pgac_option";;=0A+=20=20=20=20= esac=0A+=20=20done=0A+=20=20for=20pgac_option=20in=20$LIBCURL_LIBS;=20do=0A= +=20=20=20=20case=20$pgac_option=20in=0A+=20=20=20=20=20=20-L*)=20= LDFLAGS=3D"$LDFLAGS=20$pgac_option";;=0A+=20=20=20=20esac=0A+=20=20done=0A= +=0A+=20=20#=20OAuth=20requires=20python=20for=20testing=0A+=20=20if=20= test=20"$with_python"=20!=3D=20yes;=20then=0A+=20=20=20=20= AC_MSG_WARN([***=20OAuth=20support=20tests=20require=20--with-python=20= to=20run])=0A+=20=20fi=0A+fi=0A+=0A+=0A=20#=0A=20#=20XML=0A=20#=0A@@=20= -1294,6=20+1328,13=20@@=20failure.=20=20It=20is=20possible=20the=20= compiler=20isn't=20looking=20in=20the=20proper=20directory.=0A=20Use=20= --without-zlib=20to=20disable=20zlib=20support.])])=0A=20fi=0A=20=0A+#=20= XXX=20libcurl=20must=20link=20after=20libgssapi_krb5=20on=20FreeBSD=20to=20= avoid=20segfaults=0A+#=20during=20gss_acquire_cred().=20This=20is=20= possibly=20related=20to=20Curl's=20Heimdal=0A+#=20dependency=20on=20that=20= platform?=0A+if=20test=20"$with_libcurl"=20=3D=20yes=20;=20then=0A+=20=20= PGAC_CHECK_LIBCURL=0A+fi=0A+=0A=20if=20test=20"$with_gssapi"=20=3D=20yes=20= ;=20then=0A=20=20=20if=20test=20"$PORTNAME"=20!=3D=20"win32";=20then=0A=20= =20=20=20=20AC_SEARCH_LIBS(gss_store_cred_into,=20[gssapi_krb5=20gss=20= 'gssapi=20-lkrb5=20-lcrypto'],=20[],=0Adiff=20--git=20= a/doc/src/sgml/client-auth.sgml=20b/doc/src/sgml/client-auth.sgml=0A= index=20782b49c85ac..f84085dbac4=20100644=0A---=20= a/doc/src/sgml/client-auth.sgml=0A+++=20b/doc/src/sgml/client-auth.sgml=0A= @@=20-656,6=20+656,16=20@@=20include_dir=20=20=20=20=20=20=20=20=20= directory=0A=20=20=20=20=20=20=20=20=20=20= =0A=20=20=20=20=20=20=20=20=20=0A=20=20=20=20=20=20=20=20= =0A+=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20= =20=20=20=20oauth=0A+=20=20=20=20=20=20=20= =20=0A+=20=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20= =20=20=20Authorize=20and=20optionally=20authenticate=20using=20a=20= third-party=20OAuth=202.0=0A+=20=20=20=20=20=20=20=20=20=20identity=20= provider.=20See=20=20for=20details.=0A+=20= =20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=20=0A=20=20=20=20=20=20=20= =0A=20=0A=20=20=20=20=20=20=20=0A@@=20-1143,6=20= +1153,12=20@@=20omicron=20=20=20=20=20=20=20=20=20bryanh=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20guest1=0A=20=20=20=20=20=20=20only=20= on=20OpenBSD).=0A=20=20=20=20=20=20=0A=20=20=20=20=20=0A= +=20=20=20=20=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20= OAuth=20= authorization/authentication,=0A+=20=20=20=20=20=20which=20relies=20= on=20an=20external=20OAuth=202.0=20identity=20provider.=0A+=20=20=20=20=20= =0A+=20=20=20=20=0A=20=20=20=20=0A=20=20= =20=0A=20=0A@@=20-2329,6=20+2345,242=20@@=20host=20...=20radius=20= radiusservers=3D"server1,server2"=20radiussecrets=3D"""secret=20one"",""=0A= =20=20=20=20=0A=20=20=20=0A=20=0A+=20=20=0A+=20=20=20OAuth=20= Authorization/Authentication=0A+=0A+=20=20=20=0A+=20=20=20=20OAuth=20= Authorization/Authentication=0A+=20=20=20=0A+=0A+=20= =20=20=0A+=20=20=20=20OAuth=202.0=20is=20an=20industry-standard=20= framework,=20defined=20in=0A+=20=20=20=20RFC=20= 6749,=0A+=20=20=20=20to=20enable=20third-party=20applications=20= to=20obtain=20limited=20access=20to=20a=20protected=0A+=20=20=20=20= resource.=0A+=0A+=20=20=20=20OAuth=20client=20support=20has=20to=20be=20= enabled=20when=20PostgreSQL=0A+=20=20=20=20is=20= built,=20see=20=20for=20more=20= information.=0A+=20=20=20=0A+=0A+=20=20=20=0A+=20=20=20=20= This=20documentation=20uses=20the=20following=20terminology=20when=20= discussing=20the=20OAuth=0A+=20=20=20=20ecosystem:=0A+=0A+=20=20=20=20= =0A+=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20= Resource=20Owner=20(or=20End=20User)=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20The=20= user=20or=20system=20who=20owns=20protected=20resources=20and=20can=20= grant=20access=20to=0A+=20=20=20=20=20=20=20=20them.=20This=20= documentation=20also=20uses=20the=20term=20end=20= user=0A+=20=20=20=20=20=20=20=20when=20the=20resource=20owner=20= is=20a=20person.=20When=20you=20use=0A+=20=20=20=20=20=20=20=20= psql=20to=20connect=20to=20the=20database=20= using=20OAuth,=0A+=20=20=20=20=20=20=20=20you=20are=20the=20resource=20= owner/end=20user.=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=0A+=0A+=20=20=20=20=20= =0A+=20=20=20=20=20=20Client=0A+=20=20=20=20=20= =20=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20= The=20system=20which=20accesses=20the=20protected=20resources=20using=20= access=0A+=20=20=20=20=20=20=20=20tokens.=20Applications=20using=20= libpq,=20such=20as=20psql,=0A+=20=20=20=20=20=20= =20=20are=20the=20OAuth=20clients=20when=20connecting=20to=20a=0A+=20=20=20= =20=20=20=20=20PostgreSQL=20cluster.=0A+=20=20= =20=20=20=20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20= Resource=20Server=0A+=20=20=20=20=20=20=0A+=20=20=20= =20=20=20=20=0A+=20=20=20=20=20=20=20=20The=20system=20which=20= hosts=20the=20protected=20resources=20which=20are=0A+=20=20=20=20=20=20=20= =20accessed=20by=20the=20client.=20The=20= PostgreSQL=0A+=20=20=20=20=20=20=20=20cluster=20= being=20connected=20to=20is=20the=20resource=20server.=0A+=20=20=20=20=20= =20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20= Provider=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =20=0A+=20=20=20=20=20=20=20=20The=20organization,=20product=20= vendor,=20or=20other=20entity=20which=20develops=20and/or=0A+=20=20=20=20= =20=20=20=20administers=20the=20OAuth=20servers=20and=20clients=20for=20= a=20given=20application.=0A+=20=20=20=20=20=20=20=20Different=20= providers=20typically=20choose=20different=20implementation=20details=0A= +=20=20=20=20=20=20=20=20for=20their=20OAuth=20systems;=20a=20client=20= of=20one=20provider=20is=20not=20generally=0A+=20=20=20=20=20=20=20=20= guaranteed=20to=20have=20access=20to=20the=20servers=20of=20another.=0A+=20= =20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20= =20=20=20This=20use=20of=20the=20term=20"provider"=20is=20not=20= standard,=20but=20it=20seems=20to=20be=20in=0A+=20=20=20=20=20=20=20=20= wide=20use=20colloquially.=20(It=20should=20not=20be=20confused=20with=20= OpenID's=20similar=0A+=20=20=20=20=20=20=20=20term=20"Identity=20= Provider".=20While=20the=20implementation=20of=20OAuth=20in=0A+=20=20=20=20= =20=20=20=20PostgreSQL=20is=20intended=20to=20= be=20interoperable=0A+=20=20=20=20=20=20=20=20and=20compatible=20with=20= OpenID=20Connect/OIDC,=20it=20is=20not=20itself=20an=20OIDC=20client=0A+=20= =20=20=20=20=20=20=20and=20does=20not=20require=20its=20use.)=0A+=20=20=20= =20=20=20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20= Authorization=20Server=0A+=20=20=20=20=20=20=0A+=20= =20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20The=20system=20which=20= receives=20requests=20from,=20and=20issues=20access=20tokens=20to,=0A+=20= =20=20=20=20=20=20=20the=20client=20after=20the=20authenticated=20= resource=20owner=20has=20given=20approval.=0A+=20=20=20=20=20=20=20=20= PostgreSQL=20does=20not=20provide=20an=20= authorization=0A+=20=20=20=20=20=20=20=20server;=20it's=20obtained=20= from=20the=20OAuth=20provider.=0A+=20=20=20=20=20=20=20=0A+=20=20=20= =20=20=20=0A+=20=20=20=20=20=0A+=0A+=20=20=20=20= =20=0A+=20=20=20=20=20=20Issuer=0A+=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20An=20identifier=20= for=20an=20authorization=20server,=20printed=20as=20an=0A+=20=20=20=20=20= =20=20=20https://=20URL,=20which=20provides=20a=20= trusted=20"namespace"=0A+=20=20=20=20=20=20=20=20for=20OAuth=20clients=20= and=20applications.=20The=20issuer=20identifier=20allows=20a=0A+=20=20=20= =20=20=20=20=20single=20authorization=20server=20to=20talk=20to=20the=20= clients=20of=20mutually=0A+=20=20=20=20=20=20=20=20untrusting=20= entities,=20as=20long=20as=20they=20maintain=20separate=20issuers.=0A+=20= =20=20=20=20=20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20= =20=0A+=0A+=20=20=20=20=0A+=0A+=20=20=20=20= =0A+=20=20=20=20=20=0A+=20=20=20=20=20=20For=20small=20= deployments,=20there=20may=20not=20be=20a=20meaningful=20distinction=20= between=0A+=20=20=20=20=20=20the=20"provider",=20"authorization=20= server",=20and=20"issuer".=20However,=20for=20more=0A+=20=20=20=20=20=20= complicated=20setups,=20there=20may=20be=20a=20one-to-many=20(or=20= many-to-many)=0A+=20=20=20=20=20=20relationship:=20a=20provider=20may=20= rent=20out=20multiple=20issuer=20identifiers=20to=0A+=20=20=20=20=20=20= separate=20tenants,=20then=20provide=20multiple=20authorization=20= servers,=20possibly=0A+=20=20=20=20=20=20with=20different=20supported=20= feature=20sets,=20to=20interact=20with=20their=20clients.=0A+=20=20=20=20= =20=0A+=20=20=20=20=0A+=20=20=20=0A+=0A+=20=20=20= =0A+=20=20=20=20PostgreSQL=20supports=20= bearer=20tokens,=20defined=20in=0A+=20=20=20=20RFC=20= 6750,=0A+=20=20=20=20which=20are=20a=20type=20of=20access=20= token=20used=20with=20OAuth=202.0=20where=20the=20token=20is=20an=0A+=20=20= =20=20opaque=20string.=20=20The=20format=20of=20the=20access=20token=20= is=20implementation=20specific=0A+=20=20=20=20and=20is=20chosen=20by=20= each=20authorization=20server.=0A+=20=20=20=0A+=0A+=20=20=20= =0A+=20=20=20=20The=20following=20configuration=20options=20are=20= supported=20for=20OAuth:=0A+=20=20=20=20=0A+=20=20=20=20=20= =0A+=20=20=20=20=20=20= issuer=0A+=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20An=20HTTPS=20URL=20= which=20is=20either=20the=20exact=0A+=20=20=20=20=20=20=20=20issuer=20identifier=20of=20the=0A+=20= =20=20=20=20=20=20=20authorization=20server,=20as=20defined=20by=20its=20= discovery=20document,=20or=20a=0A+=20=20=20=20=20=20=20=20well-known=20= URI=20that=20points=20directly=20to=20that=20discovery=20document.=20= This=0A+=20=20=20=20=20=20=20=20parameter=20is=20required.=0A+=20=20=20=20= =20=20=20=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20= When=20an=20OAuth=20client=20connects=20to=20the=20server,=20a=20URL=20= for=20the=20discovery=0A+=20=20=20=20=20=20=20=20document=20will=20be=20= constructed=20using=20the=20issuer=20identifier.=20By=20default,=0A+=20=20= =20=20=20=20=20=20this=20URL=20uses=20the=20conventions=20of=20OpenID=20= Connect=20Discovery:=20the=20path=0A+=20=20=20=20=20=20=20=20= /.well-known/openid-configuration=20will=20be=20= appended=0A+=20=20=20=20=20=20=20=20to=20the=20end=20of=20the=20issuer=20= identifier.=20Alternatively,=20if=20the=0A+=20=20=20=20=20=20=20=20= issuer=20contains=20a=20= /.well-known/=0A+=20=20=20=20=20=20=20=20path=20= segment,=20that=20URL=20will=20be=20provided=20to=20the=20client=20= as-is.=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20=20The=20= OAuth=20client=20in=20libpq=20requires=20the=20server's=20issuer=20= setting=20to=0A+=20=20=20=20=20=20=20=20=20exactly=20match=20the=20= issuer=20identifier=20which=20is=20provided=20in=20the=20discovery=0A+=20= =20=20=20=20=20=20=20=20document,=20which=20must=20in=20turn=20match=20= the=20client's=0A+=20=20=20=20=20=20=20=20=20=20setting.=20No=20variations=20= in=0A+=20=20=20=20=20=20=20=20=20case=20or=20format=20are=20permitted.=0A= +=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=0A+=20= =20=20=20=20=20=0A+=20=20=20=20=20=0A+=0A+=20=20= =20=20=20=0A+=20=20=20=20=20=20= scope=0A+=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20A=20= space-separated=20list=20of=20the=20OAuth=20scopes=20needed=20for=20the=20= server=20to=0A+=20=20=20=20=20=20=20=20both=20authorize=20the=20client=20= and=20authenticate=20the=20user.=20=20Appropriate=20values=0A+=20=20=20=20= =20=20=20=20are=20determined=20by=20the=20authorization=20server=20and=20= the=20OAuth=20validation=0A+=20=20=20=20=20=20=20=20module=20used=20(see=20= =20for=20more=0A+=20=20=20=20=20= =20=20=20information=20on=20validators).=20=20This=20parameter=20is=20= required.=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=0A+=0A+=20=20=20=20=20= =0A+=20=20=20=20=20=20= validator=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20The=20= library=20to=20use=20for=20validating=20bearer=20tokens.=20If=20given,=20= the=20name=20must=0A+=20=20=20=20=20=20=20=20exactly=20match=20one=20of=20= the=20libraries=20listed=20in=0A+=20=20=20=20=20=20=20=20.=20=20This=20parameter=20= is=0A+=20=20=20=20=20=20=20=20optional=20unless=20= oauth_validator_libraries=20contains=0A+=20=20=20=20=20= =20=20=20more=20than=20one=20library,=20in=20which=20case=20it=20is=20= required.=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=0A+=0A+=20=20=20=20=20= =0A+=20=20=20=20=20=20map=0A= +=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=0A+=20=20=20= =20=20=20=20=20Allows=20for=20mapping=20between=20OAuth=20identity=20= provider=20and=20database=20user=0A+=20=20=20=20=20=20=20=20names.=20=20= See=20=20for=20details.=20=20If=20= a=0A+=20=20=20=20=20=20=20=20map=20is=20not=20specified,=20the=20user=20= name=20associated=20with=20the=20token=20(as=0A+=20=20=20=20=20=20=20=20= determined=20by=20the=20OAuth=20validator)=20must=20exactly=20match=20= the=20role=20name=0A+=20=20=20=20=20=20=20=20being=20requested.=20=20= This=20parameter=20is=20optional.=0A+=20=20=20=20=20=20=20=0A+=20=20= =20=20=20=20=0A+=20=20=20=20=20=0A+=0A+=20=20=20= =20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20= delegate_ident_mapping=0A+=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=0A+=20=20=20= =20=20=20=20=20An=20advanced=20option=20which=20is=20not=20intended=20= for=20common=20use.=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =20=0A+=20=20=20=20=20=20=20=20When=20set=20to=20= 1,=20standard=20user=20mapping=20with=0A+=20=20=20=20=20= =20=20=20pg_ident.conf=20is=20skipped,=20and=20the=20= OAuth=20validator=0A+=20=20=20=20=20=20=20=20takes=20full=20= responsibility=20for=20mapping=20end=20user=20identities=20to=20database=0A= +=20=20=20=20=20=20=20=20roles.=20=20If=20the=20validator=20authorizes=20= the=20token,=20the=20server=20trusts=20that=0A+=20=20=20=20=20=20=20=20= the=20user=20is=20allowed=20to=20connect=20under=20the=20requested=20= role,=20and=20the=0A+=20=20=20=20=20=20=20=20connection=20is=20allowed=20= to=20proceed=20regardless=20of=20the=20authentication=0A+=20=20=20=20=20=20= =20=20status=20of=20the=20user.=0A+=20=20=20=20=20=20=20=0A+=20=20= =20=20=20=20=20=0A+=20=20=20=20=20=20=20=20This=20parameter=20is=20= incompatible=20with=20map.=0A+=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=20=20= delegate_ident_mapping=20provides=20additional=0A+=20=20= =20=20=20=20=20=20=20flexibility=20in=20the=20design=20of=20the=20= authentication=20system,=20but=20it=20also=0A+=20=20=20=20=20=20=20=20=20= requires=20careful=20implementation=20of=20the=20OAuth=20validator,=20= which=20must=0A+=20=20=20=20=20=20=20=20=20determine=20whether=20the=20= provided=20token=20carries=20sufficient=20end-user=0A+=20=20=20=20=20=20=20= =20=20privileges=20in=20addition=20to=20the=20standard=0A+=20=20=20=20=20=20=20=20=20= checks=20required=20of=20all=20validators.=20=20Use=20with=20= caution.=0A+=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=20=20=20=20=0A+=20=20=20=0A+=20= =20=0A+=0A=20=20=20= =0A=20=20=20=20Authentication=20Problems=0A=20=0Adiff=20= --git=20a/doc/src/sgml/config.sgml=20b/doc/src/sgml/config.sgml=0Aindex=20= 5e4f201e099..6591a54124c=20100644=0A---=20a/doc/src/sgml/config.sgml=0A= +++=20b/doc/src/sgml/config.sgml=0A@@=20-1209,6=20+1209,32=20@@=20= include_dir=20'conf.d'=0A=20=20=20=20=20=20=20=20=0A=20=20=20=20=20= =20=20=0A=20=20=20=20=20=20=0A+=0A+=20=20=20=20= =20=0A+=20=20=20=20=20=20= oauth_validator_libraries=20= (string)=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =20=20oauth_validator_libraries=20= configuration=20parameter=0A+=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20= =20=20=20=0A+=20=20=20=20=20=20=20=20The=20library/libraries=20to=20= use=20for=20validating=20OAuth=20connection=20tokens.=20If=0A+=20=20=20=20= =20=20=20=20only=20one=20validator=20library=20is=20provided,=20it=20= will=20be=20used=20by=20default=20for=0A+=20=20=20=20=20=20=20=20any=20= OAuth=20connections;=20otherwise,=20all=0A+=20=20=20=20=20=20=20=20oauth=20HBA=20entries=0A= +=20=20=20=20=20=20=20=20must=20explicitly=20set=20a=20= validator=20chosen=20from=20this=0A+=20=20=20=20=20=20= =20=20list.=20If=20set=20to=20an=20empty=20string=20(the=20default),=20= OAuth=20connections=20will=20be=0A+=20=20=20=20=20=20=20=20refused.=20= This=20parameter=20can=20only=20be=20set=20in=20the=0A+=20=20=20=20=20=20= =20=20postgresql.conf=20file.=0A+=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20= Validator=20modules=20must=20be=20implemented/obtained=20separately;=0A+=20= =20=20=20=20=20=20=20PostgreSQL=20does=20not=20= ship=20with=20any=20default=0A+=20=20=20=20=20=20=20=20implementations.=20= For=20more=20information=20on=20implementing=20OAuth=20validators,=0A+=20= =20=20=20=20=20=20=20see=20.=0A= +=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=0A+=20=20=20= =20=20=0A=20=20=20=20=20=20=0A=20=20=20=20=20= =20=0A=20=0Adiff=20--git=20a/doc/src/sgml/filelist.sgml=20= b/doc/src/sgml/filelist.sgml=0Aindex=2066e6dccd4c9..25fb99cee69=20100644=0A= ---=20a/doc/src/sgml/filelist.sgml=0A+++=20b/doc/src/sgml/filelist.sgml=0A= @@=20-111,6=20+111,7=20@@=0A=20=0A=20=0A=20=0A+=0A=20=0A=20=0A= =20=0A= diff=20--git=20a/doc/src/sgml/installation.sgml=20= b/doc/src/sgml/installation.sgml=0Aindex=203f0a7e9c069..96e433179b9=20= 100644=0A---=20a/doc/src/sgml/installation.sgml=0A+++=20= b/doc/src/sgml/installation.sgml=0A@@=20-1143,6=20+1143,19=20@@=20= build-postgresql:=0A=20=20=20=20=20=20=20=20=0A=20=20=20=20=20= =20=20=0A=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20=20= Build=20with=20libcurl=20support=20for=20OAuth=202.0=20client=20flows.=0A= +=20=20=20=20=20=20=20=20=20This=20requires=20the=20= curl=20package=20to=20be=0A+=20=20=20=20=20=20= =20=20=20installed.=20=20Building=20with=20this=20will=20check=20for=20= the=20required=20header=20files=0A+=20=20=20=20=20=20=20=20=20and=20= libraries=20to=20make=20sure=20that=20your=20= curl=0A+=20=20=20=20=20=20=20=20=20= installation=20is=20sufficient=20before=20proceeding.=0A+=20=20=20=20=20=20= =20=20=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =0A+=0A=20=20=20=20=20=20=20=0A=20=20=20=20=20=20=20=20= =0A=20=20=20=20=20=20=20=20= =0A@@=20-2584,6=20+2597,20=20@@=20ninja=20install=0A=20=20=20=20= =20=20=20=0A=20=20=20=20=20=20=0A=20=0A+=20=20=20= =20=20=0A+=20=20=20=20= =20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=20Build=20with=20libcurl=20support=20for=20= OAuth=202.0=20client=20flows.=0A+=20=20=20=20=20=20=20=20This=20requires=20= the=20curl=20package=20to=20be=0A+=20=20=20=20= =20=20=20=20installed.=20=20Building=20with=20this=20will=20check=20for=20= the=20required=20header=20files=0A+=20=20=20=20=20=20=20=20and=20= libraries=20to=20make=20sure=20that=20your=20= curl=0A+=20=20=20=20=20=20=20=20installation=20= is=20sufficient=20before=20proceeding.=20The=20default=20for=20this=0A+=20= =20=20=20=20=20=20=20option=20is=20auto.=0A+=20=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=0A+=20=20=20=20=20=0A+=0A=20= =20=20=20=20=20=0A=20=20= =20=20=20=20=20=0A=20=20=20=20=20=20=20=0Adiff=20= --git=20a/doc/src/sgml/libpq.sgml=20b/doc/src/sgml/libpq.sgml=0Aindex=20= c49e975b082..a51355e238f=20100644=0A---=20a/doc/src/sgml/libpq.sgml=0A= +++=20b/doc/src/sgml/libpq.sgml=0A@@=20-1385,6=20+1385,15=20@@=20= postgresql://%2Fvar%2Flib%2Fpostgresql/dbname=0A=20=20=20=20=20=20=20=20=20= =20=20=0A=20=20=20=20=20=20=20=20=20=20=0A=20=0A= +=20=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20=20= =20oauth=0A+=20=20=20=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =20=20=20=20=20=20The=20server=20must=20request=20an=20OAuth=20bearer=20= token=20from=20the=20client.=0A+=20=20=20=20=20=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20=20= =0A+=0A=20=20=20=20=20=20=20=20=20=20=0A=20=20= =20=20=20=20=20=20=20=20=20none=0A=20=20=20= =20=20=20=20=20=20=20=20=0A@@=20-2373,6=20+2382,106=20@@=20= postgresql://%2Fvar%2Flib%2Fpostgresql/dbname=0A=20=20=20=20=20=20=20=20= =0A=20=20=20=20=20=20=20=0A=20=20=20=20=20=20= =0A+=0A+=20=20=20=20=20=0A+=20=20= =20=20=20=20oauth_issuer=0A+=20=20=20=20=20= =20=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20= The=20HTTPS=20URL=20of=20a=20trusted=20issuer=20to=20contact=20if=20the=20= server=20requests=20an=0A+=20=20=20=20=20=20=20=20OAuth=20token=20for=20= the=20connection.=20This=20parameter=20is=20required=20for=20all=20OAuth=0A= +=20=20=20=20=20=20=20=20connections;=20it=20should=20exactly=20match=20= the=20issuer=0A+=20=20=20=20=20=20=20=20setting=20in=20= the=20server's=20HBA=20= configuration.=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =20=0A+=20=20=20=20=20=20=20=20As=20part=20of=20the=20standard=20= authentication=20handshake,=20libpq=0A+=20=20=20= =20=20=20=20=20will=20ask=20the=20server=20for=20a=20discovery=20= document:=20a=20URL=0A+=20=20=20=20=20=20=20=20providing=20a=20= set=20of=20OAuth=20configuration=20parameters.=20The=20server=20must=0A+=20= =20=20=20=20=20=20=20provide=20a=20URL=20that=20is=20directly=20= constructed=20from=20the=20components=20of=20the=0A+=20=20=20=20=20=20=20= =20oauth_issuer,=20and=20this=20value=20must=20= exactly=20match=20the=0A+=20=20=20=20=20=20=20=20issuer=20identifier=20= that=20is=20declared=20in=20the=20discovery=20document=20itself,=20or=0A= +=20=20=20=20=20=20=20=20the=20connection=20will=20fail.=20This=20is=20= required=20to=20prevent=20a=20class=20of=20"mix-up=0A+=20=20=20=20=20=20=20= =20attacks"=20on=20OAuth=20clients.=0A+=20=20=20=20=20=20=20=0A+=20= =20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20You=20may=20also=20= explicitly=20set=20oauth_issuer=20to=20the=0A+=20=20=20= =20=20=20=20=20/.well-known/=20URI=20used=20for=20= OAuth=20discovery.=20In=20this=0A+=20=20=20=20=20=20=20=20case,=20if=20= the=20server=20asks=20for=20a=20different=20URL,=20the=20connection=20= will=20fail,=0A+=20=20=20=20=20=20=20=20but=20a=20custom=20OAuth=20flow=0A+=20= =20=20=20=20=20=20=20may=20be=20able=20to=20speed=20up=20the=20standard=20= handshake=20by=20using=20previously=0A+=20=20=20=20=20=20=20=20cached=20= tokens.=20(In=20this=20case,=20it=20is=20recommended=20that=0A+=20=20=20=20= =20=20=20=20=20be=20set=20= as=20well,=20since=20the=0A+=20=20=20=20=20=20=20=20client=20will=20not=20= have=20a=20chance=20to=20ask=20the=20server=20for=20a=20correct=20scope=0A= +=20=20=20=20=20=20=20=20setting,=20and=20the=20default=20scopes=20for=20= a=20token=20may=20not=20be=20sufficient=20to=0A+=20=20=20=20=20=20=20=20= connect.)=20libpq=20currently=20supports=20= the=0A+=20=20=20=20=20=20=20=20following=20well-known=20endpoints:=0A+=20= =20=20=20=20=20=20=20=0A+=20=20=20=20= =20=20=20=20=20= /.well-known/openid-configuration=0A+=20=20=20=20=20=20=20=20=20= /.well-known/oauth-authorization-server= =0A+=20=20=20=20=20=20=20=20=0A+=20=20=20= =20=20=20=20=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =20=20=0A+=20=20=20=20=20=20=20=20=20Issuers=20are=20highly=20= privileged=20during=20the=20OAuth=20connection=20handshake.=20As=0A+=20=20= =20=20=20=20=20=20=20a=20rule=20of=20thumb,=20if=20you=20would=20not=20= trust=20the=20operator=20of=20a=20URL=20to=20handle=0A+=20=20=20=20=20=20= =20=20=20access=20to=20your=20servers,=20or=20to=20impersonate=20you=20= directly,=20that=20URL=20should=0A+=20=20=20=20=20=20=20=20=20not=20be=20= trusted=20as=20an=20oauth_issuer.=0A+=20=20=20=20=20=20= =20=20=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=0A+=0A+=20=20=20=20=20= =0A+=20=20=20=20=20=20= oauth_client_id=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20An=20= OAuth=202.0=20client=20identifier,=20as=20issued=20by=20the=20= authorization=20server.=0A+=20=20=20=20=20=20=20=20If=20the=20= PostgreSQL=20server=0A+=20=20=20=20=20=20=20=20= requests=20an=20OAuth=20token=20= for=20the=0A+=20=20=20=20=20=20=20=20connection=20(and=20if=20no=20custom=0A+=20=20=20=20=20=20=20=20= OAuth=20hook=20is=20installed=20to=20provide=20one),=20then=20= this=20parameter=20must=0A+=20=20=20=20=20=20=20=20be=20set;=20= otherwise,=20the=20connection=20will=20fail.=0A+=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20= oauth_client_secret=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20The=20= client=20password,=20if=20any,=20to=20use=20when=20contacting=20the=20= OAuth=0A+=20=20=20=20=20=20=20=20authorization=20server.=20Whether=20= this=20parameter=20is=20required=20or=20not=20is=0A+=20=20=20=20=20=20=20= =20determined=20by=20the=20OAuth=20provider;=20"public"=20clients=20= generally=20do=20not=20use=0A+=20=20=20=20=20=20=20=20a=20secret,=20= whereas=20"confidential"=20clients=20generally=20do.=0A+=20=20=20=20=20=20= =20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=0A+=20=20=20=20=20=0A+=20=20=20= =20=20=20oauth_scope=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20The=20= scope=20of=20the=20access=20request=20sent=20to=20the=20authorization=20= server,=0A+=20=20=20=20=20=20=20=20specified=20as=20a=20(possibly=20= empty)=20space-separated=20list=20of=20OAuth=20scope=0A+=20=20=20=20=20=20= =20=20identifiers.=20This=20parameter=20is=20optional=20and=20intended=20= for=20advanced=20usage.=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20= =20=20=0A+=20=20=20=20=20=20=20=20Usually=20the=20client=20will=20= obtain=20appropriate=20scope=20settings=20from=20the=0A+=20=20=20=20=20=20= =20=20PostgreSQL=20server.=20If=20this=20= parameter=20is=20used,=0A+=20=20=20=20=20=20=20=20the=20server's=20= requested=20scope=20list=20will=20be=20ignored.=20This=20can=20prevent=20= a=0A+=20=20=20=20=20=20=20=20less-trusted=20server=20from=20requesting=20= inappropriate=20access=20scopes=20from=20the=0A+=20=20=20=20=20=20=20=20= end=20user.=20However,=20if=20the=20client's=20scope=20setting=20does=20= not=20contain=20the=0A+=20=20=20=20=20=20=20=20server's=20required=20= scopes,=20the=20server=20is=20likely=20to=20reject=20the=20issued=0A+=20=20= =20=20=20=20=20=20token,=20and=20the=20connection=20will=20fail.=0A+=20=20= =20=20=20=20=20=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =20=20The=20meaning=20of=20an=20empty=20scope=20list=20is=20= provider-dependent.=20An=20OAuth=0A+=20=20=20=20=20=20=20=20= authorization=20server=20may=20choose=20to=20issue=20a=20token=20with=20= "default=20scope",=0A+=20=20=20=20=20=20=20=20whatever=20that=20happens=20= to=20be,=20or=20it=20may=20reject=20the=20token=20request=0A+=20=20=20=20= =20=20=20=20entirely.=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=0A+=0A=20=20=20=20=20= =0A=20=20=20=20=0A=20=20=20=0A@@=20= -10020,6=20+10129,291=20@@=20void=20PQinitSSL(int=20do_ssl);=0A=20=0A=20=20= =0A=20=0A+=20=0A+=20=20OAuth=20= Support=0A+=0A+=20=20=0A+=20=20=20TODO=0A+=20=20=0A= +=0A+=20=20=0A+=20=20=20= Authdata=20Hooks=0A+=0A+=20=20=20=0A+=20=20=20=20= The=20behavior=20of=20the=20OAuth=20flow=20may=20be=20modified=20or=20= replaced=20by=20a=20client=20using=0A+=20=20=20=20the=20following=20hook=20= API:=0A+=0A+=20=20=20=20=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20= PQsetAuthDataHookPQsetAuthD= ataHook=0A+=0A+=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20Sets=20the=20= PGauthDataHook,=20overriding=0A+=20=20=20=20=20=20=20=20= libpq's=20handling=20of=20one=20or=20more=20= aspects=20of=0A+=20=20=20=20=20=20=20=20its=20OAuth=20client=20flow.=0A= +=0A+void=20PQsetAuthDataHook(PQauthDataHook_type=20hook);=0A= +=0A+=20=20=20=20=20=20=20=20If=20= hook=20is=20NULL,=20the=0A= +=20=20=20=20=20=20=20=20default=20handler=20will=20be=20reinstalled.=20= Otherwise,=20the=20application=20passes=0A+=20=20=20=20=20=20=20=20a=20= pointer=20to=20a=20callback=20function=20with=20the=20signature:=0A= +=0A+int=20hook_fn(PGauthData=20type,=20PGconn=20*conn,=20= void=20*data);=0A+=0A+=20=20=20=20=20=20=20=20which=20= libpq=20will=20call=20when=20when=20action=20= is=0A+=20=20=20=20=20=20=20=20required=20of=20the=20application.=20= type=20describes=0A+=20=20=20=20=20=20=20=20= the=20request=20being=20made,=20conn=20is=20= the=0A+=20=20=20=20=20=20=20=20connection=20handle=20being=20= authenticated,=20and=20data=0A+=20=20=20=20=20= =20=20=20points=20to=20request-specific=20metadata.=20The=20contents=20= of=20this=20pointer=20are=0A+=20=20=20=20=20=20=20=20determined=20by=20= type;=20see=0A+=20=20=20=20=20=20=20=20=20for=20the=20supported=0A= +=20=20=20=20=20=20=20=20list.=0A+=20=20=20=20=20=20=20=0A+=20=20=20= =20=20=20=20=0A+=20=20=20=20=20=20=20=20Hooks=20can=20be=20chained=20= together=20to=20allow=20cooperative=20and/or=20fallback=0A+=20=20=20=20=20= =20=20=20behavior.=20In=20general,=20a=20hook=20implementation=20should=20= examine=20the=20incoming=0A+=20=20=20=20=20=20=20=20= type=20(and,=20potentially,=20the=20request=20= metadata=0A+=20=20=20=20=20=20=20=20and/or=20the=20settings=20for=20the=20= particular=20conn=0A+=20=20=20=20=20=20=20=20= in=20use)=20to=20decide=20whether=20or=20not=20to=20handle=20a=20= specific=20piece=20of=20authdata.=0A+=20=20=20=20=20=20=20=20If=20not,=20= it=20should=20delegate=20to=20the=20previous=20hook=20in=20the=20chain=0A= +=20=20=20=20=20=20=20=20(retrievable=20via=20= PQgetAuthDataHook).=0A+=20=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20Success=20is=20= indicated=20by=20returning=20an=20integer=20greater=20than=20zero.=0A+=20= =20=20=20=20=20=20=20Returning=20a=20negative=20integer=20signals=20an=20= error=20condition=20and=20abandons=20the=0A+=20=20=20=20=20=20=20=20= connection=20attempt.=20(A=20zero=20value=20is=20reserved=20for=20the=20= default=0A+=20=20=20=20=20=20=20=20implementation.)=0A+=20=20=20=20=20=20= =20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20= PQgetAuthDataHookPQgetAuthD= ataHook=0A+=0A+=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20Retrieves=20the=20= current=20value=20of=20PGauthDataHook.=0A+=0A= +PQauthDataHook_type=20PQgetAuthDataHook(void);=0A+=0A+=20=20=20= =20=20=20=20=20At=20initialization=20time=20(before=20the=20first=20call=20= to=0A+=20=20=20=20=20=20=20=20PQsetAuthDataHook),=20= this=20function=20will=20return=0A+=20=20=20=20=20=20=20=20= PQdefaultAuthDataHook.=0A+=20=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=0A+=20=20=20=20=20=0A+=20=20= =20=20=0A+=20=20=20=0A+=0A+=20=20=20=0A+=20=20=20=20Hook=20= Types=0A+=20=20=20=20=0A+=20=20=20=20=20The=20following=20= PGauthData=20types=20and=20their=20corresponding=0A+=20=20= =20=20=20data=20structures=20are=20defined:=0A= +=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=20= PQAUTHDATA_PROMPT_OAUTH_DEVICE=0A+=20=20=20=20=20=20=20=20= PQAUTHDATA_PROMPT_OAUTH_DEVICE=0A= +=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=0A+=20=20= =20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20=20Replaces=20the=20= default=20user=20prompt=20during=20the=20builtin=20device=0A+=20=20=20=20= =20=20=20=20=20authorization=20client=20flow.=20= data=20points=20to=0A+=20=20=20=20=20=20=20=20= =20an=20instance=20of=20PGpromptOAuthDevice:=0A= +=0A+typedef=20struct=20_PGpromptOAuthDevice=0A+{=0A+=20=20=20=20= const=20char=20*verification_uri;=20=20=20/*=20verification=20URI=20to=20= visit=20*/=0A+=20=20=20=20const=20char=20*user_code;=20=20=20=20=20=20=20= =20=20=20/*=20user=20code=20to=20enter=20*/=0A+=20=20=20=20const=20char=20= *verification_uri_complete;=20=20/*=20optional=20combination=20of=20URI=20= and=0A+=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20*=20= code,=20or=20NULL=20*/=0A+=20=20=20=20int=20=20=20=20=20=20=20=20=20= expires_in;=20=20=20=20=20=20=20=20=20/*=20seconds=20until=20user=20code=20= expires=20*/=0A+}=20PGpromptOAuthDevice;=0A+=0A+=20=20=20=20=20= =20=20=20=0A+=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20= =20=20The=20OAuth=20Device=20Authorization=20flow=20included=20in=20= libpq=0A+=20=20=20=20=20=20=20=20=20requires=20= the=20end=20user=20to=20visit=20a=20URL=20with=20a=20browser,=20then=20= enter=20a=20code=0A+=20=20=20=20=20=20=20=20=20which=20permits=20= libpq=20to=20connect=20to=20the=20server=0A+=20= =20=20=20=20=20=20=20=20on=20their=20behalf.=20The=20default=20prompt=20= simply=20prints=20the=0A+=20=20=20=20=20=20=20=20=20= verification_uri=20and=20user_code=0A= +=20=20=20=20=20=20=20=20=20on=20standard=20error.=20Replacement=20= implementations=20may=20display=20this=0A+=20=20=20=20=20=20=20=20=20= information=20using=20any=20preferred=20method,=20for=20example=20with=20= a=20GUI.=0A+=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=20=20This=20callback=20is=20only=20= invoked=20during=20the=20builtin=20device=0A+=20=20=20=20=20=20=20=20=20= authorization=20flow.=20If=20the=20application=20installs=20a=0A+=20=20=20= =20=20=20=20=20=20custom=20OAuth=0A+=20= =20=20=20=20=20=20=20=20flow,=20this=20authdata=20type=20will=20= not=20be=20used.=0A+=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =20=20=0A+=20=20=20=20=20=20=20=20=20If=20a=20non-NULL=20= verification_uri_complete=20is=0A+=20=20=20=20= =20=20=20=20=20provided,=20it=20may=20optionally=20be=20used=20for=20= non-textual=20verification=20(for=0A+=20=20=20=20=20=20=20=20=20example,=20= by=20displaying=20a=20QR=20code).=20The=20URL=20and=20user=20code=20= should=20still=0A+=20=20=20=20=20=20=20=20=20be=20displayed=20to=20the=20= end=20user=20in=20this=20case,=20because=20the=20code=20will=20be=0A+=20=20= =20=20=20=20=20=20=20manually=20confirmed=20by=20the=20provider,=20and=20= the=20URL=20lets=20users=20continue=0A+=20=20=20=20=20=20=20=20=20even=20= if=20they=20can't=20use=20the=20non-textual=20method.=20For=20more=20= information,=0A+=20=20=20=20=20=20=20=20=20see=20section=203.3.1=20in=0A= +=20=20=20=20=20=20=20=20=20RFC=20= 8628.=0A+=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=0A+=0A+=20=20=20=20=20=20= =0A+=20=20= =20=20=20=20=20=0A+=20=20=20=20=20=20=20=20= PQAUTHDATA_OAUTH_BEARER_TOKEN=0A+=20=20=20=20=20=20=20=20= PQAUTHDATA_OAUTH_BEARER_TOKEN=0A= +=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=0A+=20=20= =20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20=20Replaces=20the=20= entire=20OAuth=20flow=20with=20a=20custom=20implementation.=20The=20hook=0A= +=20=20=20=20=20=20=20=20=20should=20either=20directly=20return=20a=20= Bearer=20token=20for=20the=20current=0A+=20=20=20=20=20=20=20=20=20= user/issuer/scope=20combination,=20if=20one=20is=20available=20without=20= blocking,=20or=0A+=20=20=20=20=20=20=20=20=20else=20set=20up=20an=20= asynchronous=20callback=20to=20retrieve=20one.=0A+=20=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20=20= data=20points=20to=20an=20instance=0A+=20=20=20= =20=20=20=20=20=20of=20PGoauthBearerRequest,=20which=20= should=20be=20filled=20in=0A+=20=20=20=20=20=20=20=20=20by=20the=20= implementation:=0A+=0A+typedef=20struct=20= _PGoauthBearerRequest=0A+{=0A+=20=20=20=20/*=20Hook=20inputs=20(constant=20= across=20all=20calls)=20*/=0A+=20=20=20=20const=20char=20*const=20= openid_configuration;=20/*=20OIDC=20discovery=20URL=20*/=0A+=20=20=20=20= const=20char=20*const=20scope;=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20/*=20required=20scope(s),=20or=20NULL=20*/=0A+=0A+=20=20=20=20/*=20= Hook=20outputs=20*/=0A+=0A+=20=20=20=20/*=20Callback=20implementing=20a=20= custom=20asynchronous=20OAuth=20flow.=20*/=0A+=20=20=20=20= PostgresPollingStatusType=20(*async)=20(PGconn=20*conn,=0A+=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20struct=20_PGoauthBearerRequest=20*request,=0A= +=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20SOCKTYPE=20*altsock);=0A= +=0A+=20=20=20=20/*=20Callback=20to=20clean=20up=20custom=20allocations.=20= */=0A+=20=20=20=20void=20=20=20=20=20=20=20=20(*cleanup)=20(PGconn=20= *conn,=20struct=20_PGoauthBearerRequest=20*request);=0A+=0A+=20=20=20=20= char=20=20=20=20=20=20=20*token;=20=20=20/*=20acquired=20Bearer=20token=20= */=0A+=20=20=20=20void=20=20=20=20=20=20=20*user;=20=20=20=20/*=20= hook-defined=20allocated=20data=20*/=0A+}=20PGoauthBearerRequest;=0A= +=0A+=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=20=20Two=20pieces=20of=20information=20= are=20provided=20to=20the=20hook=20by=0A+=20=20=20=20=20=20=20=20=20= libpq:=0A+=20=20=20=20=20=20=20=20=20= openid_configuration=20contains=20the=20URL=20= of=20an=0A+=20=20=20=20=20=20=20=20=20OAuth=20discovery=20document=20= describing=20the=20authorization=20server's=0A+=20=20=20=20=20=20=20=20=20= supported=20flows,=20and=20scope=20contains=20= a=0A+=20=20=20=20=20=20=20=20=20(possibly=20empty)=20space-separated=20= list=20of=20OAuth=20scopes=20which=20are=0A+=20=20=20=20=20=20=20=20=20= required=20to=20access=20the=20server.=20Either=20or=20both=20may=20be=0A= +=20=20=20=20=20=20=20=20=20NULL=20to=20indicate=20= that=20the=20information=20was=20not=0A+=20=20=20=20=20=20=20=20=20= discoverable.=20(In=20this=20case,=20implementations=20may=20be=20able=20= to=20establish=0A+=20=20=20=20=20=20=20=20=20the=20requirements=20using=20= some=20other=20preconfigured=20knowledge,=20or=20they=20may=0A+=20=20=20=20= =20=20=20=20=20choose=20to=20fail.)=0A+=20=20=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20=20The=20= final=20output=20of=20the=20hook=20is=20= token,=20which=0A+=20=20=20=20=20=20=20=20=20= must=20point=20to=20a=20valid=20Bearer=20token=20for=20use=20on=20the=20= connection.=20(This=0A+=20=20=20=20=20=20=20=20=20token=20should=20be=20= issued=20by=20the=0A+=20=20=20=20=20=20=20=20=20=20and=20hold=20the=20requested=0A= +=20=20=20=20=20=20=20=20=20scopes,=20or=20the=20connection=20will=20be=20= rejected=20by=20the=20server's=20validator=0A+=20=20=20=20=20=20=20=20=20= module.)=20The=20allocated=20token=20string=20must=20remain=20valid=20= until=0A+=20=20=20=20=20=20=20=20=20libpq=20= is=20finished=20connecting;=20the=20hook=0A+=20=20=20=20=20=20=20=20=20= should=20set=20a=20cleanup=20callback=20which=20= will=20be=0A+=20=20=20=20=20=20=20=20=20called=20when=20= libpq=20no=20longer=20requires=20it.=0A+=20=20= =20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20=0A+=20=20=20=20= =20=20=20=20=20If=20an=20implementation=20cannot=20immediately=20produce=20= a=0A+=20=20=20=20=20=20=20=20=20token=20= during=20the=20initial=20call=20to=20the=20hook,=0A+=20=20=20=20=20=20=20= =20=20it=20should=20set=20the=20async=20= callback=20to=20handle=0A+=20=20=20=20=20=20=20=20=20nonblocking=20= communication=20with=20the=20authorization=20server.=0A+=20=20=20=20=20=20= =20=20=20=0A+=20=20=20=20=20=20=20=20=20=20=0A+=20=20=20=20= =20=20=20=20=20=20=20Performing=20blocking=20operations=20during=20the=0A= +=20=20=20=20=20=20=20=20=20=20=20= PQAUTHDATA_OAUTH_BEARER_TOKEN=20hook=20callback=20will=0A= +=20=20=20=20=20=20=20=20=20=20=20interfere=20with=20nonblocking=20= connection=20APIs=20such=20as=0A+=20=20=20=20=20=20=20=20=20=20=20= PQconnectPoll=20and=20prevent=20concurrent=20= connections=0A+=20=20=20=20=20=20=20=20=20=20=20from=20making=20= progress.=20Applications=20which=20only=20ever=20use=20the=0A+=20=20=20=20= =20=20=20=20=20=20=20synchronous=20connection=20primitives,=20such=20as=0A= +=20=20=20=20=20=20=20=20=20=20=20PQconnectdb,=20= may=20synchronously=20retrieve=20a=20token=0A+=20=20=20=20=20=20=20=20=20= =20=20during=20the=20hook=20instead=20of=20implementing=20the=0A+=20=20=20= =20=20=20=20=20=20=20=20async=20callback,=20= but=20they=20will=20necessarily=0A+=20=20=20=20=20=20=20=20=20=20=20be=20= limited=20to=20one=20connection=20at=20a=20time.=0A+=20=20=20=20=20=20=20= =20=20=20=0A+=20=20=20=20=20=20=20=20=20=0A+=20=20=20=20= =20=20=20=20=20This=20will=20be=20called=20to=20begin=20the=20flow=20= immediately=20upon=20return=20from=20the=0A+=20=20=20=20=20=20=20=20=20= hook.=20When=20the=20callback=20cannot=20make=20further=20progress=20= without=20blocking,=0A+=20=20=20=20=20=20=20=20=20it=20should=20return=20= either=20PGRES_POLLING_READING=20or=0A+=20=20=20=20=20=20= =20=20=20PGRES_POLLING_WRITING=20after=20setting=0A+=20=20= =20=20=20=20=20=20=20*pgsocket=20to=20the=20file=20= descriptor=20that=20will=20be=20marked=0A+=20=20=20=20=20=20=20=20=20= ready=20to=20read/write=20when=20progress=20can=20be=20made=20again.=20= (This=20descriptor=0A+=20=20=20=20=20=20=20=20=20is=20then=20provided=20= to=20the=20top-level=20polling=20loop=20via=0A+=20=20=20=20=20=20=20=20=20= PQsocket().)=20Return=20= PGRES_POLLING_OK=0A+=20=20=20=20=20=20=20=20=20after=20= setting=20token=20when=20the=20flow=20is=0A+=20= =20=20=20=20=20=20=20=20complete,=20or=20= PGRES_POLLING_FAILED=20to=20indicate=20failure.=0A+=20=20= =20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20=0A+=20=20=20=20= =20=20=20=20=20Implementations=20may=20wish=20to=20store=20additional=20= data=20for=20bookkeeping=0A+=20=20=20=20=20=20=20=20=20across=20calls=20= to=20the=20async=20and=0A+=20=20=20=20=20=20=20= =20=20cleanup=20callbacks.=20The=0A+=20=20=20=20= =20=20=20=20=20user=20pointer=20is=20provided=20= for=20this=20purpose;=0A+=20=20=20=20=20=20=20=20=20= libpq=20will=20not=20touch=20its=20contents=20= and=20the=0A+=20=20=20=20=20=20=20=20=20application=20may=20use=20it=20= at=20its=20convenience.=20(Remember=20to=20free=20any=0A+=20=20=20=20=20=20= =20=20=20allocations=20during=20token=20cleanup.)=0A+=20=20=20=20=20=20=20= =20=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=0A+=20=20=20=20=0A= +=20=20=20=0A+=20=20=0A+=0A+=20=20=0A+=20=20=20Debugging=20and=20= Developer=20Settings=0A+=0A+=20=20=20=0A+=20=20=20=20A=20= "dangerous=20debugging=20mode"=20may=20be=20enabled=20by=20setting=20the=20= environment=0A+=20=20=20=20variable=20= PGOAUTHDEBUG=3DUNSAFE.=20This=20functionality=20is=20= provided=0A+=20=20=20=20for=20ease=20of=20local=20development=20and=20= testing=20only.=20It=20does=20several=20things=20that=0A+=20=20=20=20you=20= will=20not=20want=20a=20production=20system=20to=20do:=0A+=0A+=20=20=20=20= =0A+=20=20=20=20=20=0A+=20=20= =20=20=20=20=0A+=20=20=20=20=20=20=20permits=20the=20use=20of=20= unencrypted=20HTTP=20during=20the=20OAuth=20provider=20exchange=0A+=20=20= =20=20=20=20=0A+=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20allows=20= the=20system's=20trusted=20CA=20list=20to=20be=20completely=20replaced=20= using=20the=0A+=20=20=20=20=20=20=20PGOAUTHCAFILE=20= environment=20variable=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=20=20=20=20=20=0A+=20=20=20=20=20=20=0A+=20= =20=20=20=20=20=20sprays=20HTTP=20traffic=20(containing=20several=20= critical=20secrets)=20to=20standard=0A+=20=20=20=20=20=20=20error=20= during=20the=20OAuth=20flow=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=20=20=20=20=20=0A+=20=20=20=20=20=20=0A+=20= =20=20=20=20=20=20permits=20the=20use=20of=20zero-second=20retry=20= intervals,=20which=20can=20cause=20the=0A+=20=20=20=20=20=20=20client=20= to=20busy-loop=20and=20pointlessly=20consume=20CPU=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=0A+=20=20=20=20=0A+=20= =20=20=0A+=20=20=20=0A+=20=20=20=20=0A+=20=20=20=20= =20Do=20not=20share=20the=20output=20of=20the=20OAuth=20flow=20traffic=20= with=20third=20parties.=20It=0A+=20=20=20=20=20contains=20secrets=20that=20= can=20be=20used=20to=20attack=20your=20clients=20and=20servers.=0A+=20=20= =20=20=0A+=20=20=20=0A+=20=20=0A+=20=0A= +=0A=20=0A=20=20=0A=20=20=20= Behavior=20in=20Threaded=20Programs=0A@@=20-10092,6=20= +10486,18=20@@=20int=20PQisthreadsafe();=0A=20=20=20=20= libpq=20source=20code=20for=20a=20way=20to=20= do=20cooperative=0A=20=20=20=20locking=20between=20= libpq=20and=20your=20application.=0A=20=20=20= =0A+=0A+=20=20=0A+=20=20=20Similarly,=20if=20you=20are=20= using=20Curl=20inside=20your=20application,=0A+=20=20=20= and=20you=20do=20not=20already=0A+=20=20=20initialize=0A+=20= =20=20libcurl=20globally=20before=20starting=20new=20threads,=20= you=20will=20need=20to=0A+=20=20=20cooperatively=20lock=20(again=20via=20= PQregisterThreadLock)=0A+=20=20=20around=20any=20= code=20that=20may=20initialize=20libcurl.=20This=20restriction=20is=20= lifted=20for=0A+=20=20=20more=20recent=20versions=20of=20Curl=20that=20= are=20built=20to=20support=20threadsafe=0A+=20=20=20initialization;=20= those=20builds=20can=20be=20identified=20by=20the=20advertisement=20of=20= a=0A+=20=20=20threadsafe=20feature=20in=20their=20= version=20metadata.=0A+=20=20=0A=20=20=0A=20=0A=20=0Adiff=20= --git=20a/doc/src/sgml/oauth-validators.sgml=20= b/doc/src/sgml/oauth-validators.sgml=0Anew=20file=20mode=20100644=0A= index=2000000000000..d0bca9196d9=0A---=20/dev/null=0A+++=20= b/doc/src/sgml/oauth-validators.sgml=0A@@=20-0,0=20+1,402=20@@=0A+=0A+=0A+=0A+=20OAuth=20Validator=20= Modules=0A+=20=0A+=20=20= OAuth=20Validators=0A+=20=0A+=20=0A= +=20=20PostgreSQL=20provides=20infrastructure=20= for=20creating=0A+=20=20custom=20modules=20to=20perform=20server-side=20= validation=20of=20OAuth=20bearer=20tokens.=0A+=20=20Because=20OAuth=20= implementations=20vary=20so=20wildly,=20and=20bearer=20token=20= validation=20is=0A+=20=20heavily=20dependent=20on=20the=20issuing=20= party,=20the=20server=20cannot=20check=20the=20token=0A+=20=20itself;=20= validator=20modules=20provide=20the=20glue=20between=20the=20server=20= and=20the=20OAuth=0A+=20=20provider=20in=20use.=0A+=20=0A+=20= =0A+=20=20OAuth=20validator=20modules=20must=20at=20least=20= consist=20of=20an=20initialization=20function=0A+=20=20(see=20)=20and=20the=20required=20callback=20= for=0A+=20=20performing=20validation=20(see=20).=0A+=20=0A+=20= =0A+=20=20=0A+=20=20=20Since=20a=20misbehaving=20= validator=20might=20let=20unauthorized=20users=20into=20the=20database,=0A= +=20=20=20correct=20implementation=20is=20critical.=20See=0A+=20=20=20= =20for=20design=20= considerations.=0A+=20=20=0A+=20=0A+=0A+=20=0A+=20=20Safely=20Designing=20a=20= Validator=20Module=0A+=20=20=0A+=20=20=20=0A+=20=20= =20=20Read=20and=20understand=20the=20entirety=20of=20this=20section=20= before=20implementing=20a=0A+=20=20=20=20validator=20module.=20A=20= malfunctioning=20validator=20is=20potentially=20worse=20than=20no=0A+=20=20= =20=20authentication=20at=20all,=20both=20because=20of=20the=20false=20= sense=20of=20security=20it=0A+=20=20=20=20provides,=20and=20because=20it=20= may=20contribute=20to=20attacks=20against=20other=20pieces=20of=0A+=20=20= =20=20an=20OAuth=20ecosystem.=0A+=20=20=20=0A+=20=20=0A= +=0A+=20=20=0A+=20= =20=20Validator=20Responsibilities=0A+=20=20=20=0A+=20= =20=20=20TODO=0A+=20=20=20=0A+=20=20=20=0A+=20=20=20= =20=0A+=20=20=20=20=20Validate=20the=20Token=0A= +=20=20=20=20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =20=20The=20validator=20must=20first=20ensure=20that=20the=20presented=20= token=20is=20in=20fact=20a=0A+=20=20=20=20=20=20=20valid=20Bearer=20= token=20for=20use=20in=20client=20authentication.=20The=20correct=20way=20= to=0A+=20=20=20=20=20=20=20do=20this=20depends=20on=20the=20provider,=20= but=20it=20generally=20involves=20either=0A+=20=20=20=20=20=20=20= cryptographic=20operations=20to=20prove=20that=20the=20token=20was=20= created=20by=20a=20trusted=0A+=20=20=20=20=20=20=20party=20(offline=20= validation),=20or=20the=20presentation=20of=20the=20token=20to=20that=0A= +=20=20=20=20=20=20=20trusted=20party=20so=20that=20it=20can=20perform=20= validation=20for=20you=20(online=0A+=20=20=20=20=20=20=20validation).=0A= +=20=20=20=20=20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =20=20Online=20validation,=20usually=20implemented=20via=0A+=20=20=20=20=20= =20=20OAuth= =20Token=0A+=20=20=20=20=20=20=20Introspection,=20requires=20= fewer=20steps=20of=20a=20validator=20module=20and=0A+=20=20=20=20=20=20=20= allows=20central=20revocation=20of=20a=20token=20in=20the=20event=20that=20= it=20is=20stolen=0A+=20=20=20=20=20=20=20or=20misissued.=20However,=20it=20= does=20require=20the=20module=20to=20make=20at=20least=20one=0A+=20=20=20= =20=20=20=20network=20call=20per=20authentication=20attempt=20(all=20of=20= which=20must=20complete=0A+=20=20=20=20=20=20=20within=20the=20= configured=20).=0A+=20=20= =20=20=20=20=20Additionally,=20your=20provider=20may=20not=20provide=20= introspection=20endpoints=20for=0A+=20=20=20=20=20=20=20use=20by=20= external=20resource=20servers.=0A+=20=20=20=20=20=20=0A+=20=20=20=20= =20=20=0A+=20=20=20=20=20=20=20Offline=20validation=20is=20much=20= more=20involved,=20typically=20requiring=20a=20validator=0A+=20=20=20=20=20= =20=20to=20maintain=20a=20list=20of=20trusted=20signing=20keys=20for=20a=20= provider=20and=20then=0A+=20=20=20=20=20=20=20check=20the=20token's=20= cryptographic=20signature=20along=20with=20its=20contents.=0A+=20=20=20=20= =20=20=20Implementations=20must=20follow=20the=20provider's=20= instructions=20to=20the=20letter,=0A+=20=20=20=20=20=20=20including=20= any=20verification=20of=20issuer=20("where=20is=20this=20token=20= from?"),=0A+=20=20=20=20=20=20=20audience=20("who=20is=20this=20token=20= for?"),=20and=20validity=20period=20("when=20can=20this=0A+=20=20=20=20=20= =20=20token=20be=20used?").=20Since=20there=20is=20no=20communication=20= between=20the=20module=20and=0A+=20=20=20=20=20=20=20the=20provider,=20= tokens=20cannot=20be=20centrally=20revoked=20using=20this=20method;=0A+=20= =20=20=20=20=20=20offline=20validator=20implementations=20may=20wish=20= to=20place=20restrictions=20on=20the=0A+=20=20=20=20=20=20=20maximum=20= length=20of=20a=20token's=20validity=20period.=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20If=20the=20= token=20cannot=20be=20validated,=20the=20module=20should=20immediately=20= fail.=0A+=20=20=20=20=20=20=20Further=20authentication/authorization=20= is=20pointless=20if=20the=20bearer=20token=0A+=20=20=20=20=20=20=20= wasn't=20issued=20by=20a=20trusted=20party.=0A+=20=20=20=20=20=20=0A= +=20=20=20=20=20=0A+=20=20=20=20=0A+=20=20=20=20= =0A+=20=20=20=20=20Authorize=20the=20Client=0A= +=20=20=20=20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =20=20Next=20the=20validator=20must=20ensure=20that=20the=20end=20user=20= has=20given=20the=20client=0A+=20=20=20=20=20=20=20permission=20to=20= access=20the=20server=20on=20their=20behalf.=20This=20generally=20= involves=0A+=20=20=20=20=20=20=20checking=20the=20scopes=20that=20have=20= been=20assigned=20to=20the=20token,=20to=20make=20sure=0A+=20=20=20=20=20= =20=20that=20they=20cover=20database=20access=20for=20the=20current=20= HBA=20parameters.=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20The=20purpose=20of=20this=20step=20is=20= to=20prevent=20an=20OAuth=20client=20from=20obtaining=20a=0A+=20=20=20=20= =20=20=20token=20under=20false=20pretenses.=20If=20the=20validator=20= requires=20all=20tokens=20to=0A+=20=20=20=20=20=20=20carry=20scopes=20= that=20cover=20database=20access,=20the=20provider=20should=20then=20= loudly=0A+=20=20=20=20=20=20=20prompt=20the=20user=20to=20grant=20that=20= access=20during=20the=20flow.=20This=20gives=20them=20the=0A+=20=20=20=20= =20=20=20opportunity=20to=20reject=20the=20request=20if=20the=20client=20= isn't=20supposed=20to=20be=0A+=20=20=20=20=20=20=20using=20their=20= credentials=20to=20connect=20to=20databases.=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20While=20it=20= is=20possible=20to=20establish=20client=20authorization=20without=20= explicit=0A+=20=20=20=20=20=20=20scopes=20by=20using=20out-of-band=20= knowledge=20of=20the=20deployed=20architecture,=20doing=0A+=20=20=20=20=20= =20=20so=20removes=20the=20user=20from=20the=20loop,=20which=20prevents=20= them=20from=20catching=0A+=20=20=20=20=20=20=20deployment=20mistakes=20= and=20allows=20any=20such=20mistakes=20to=20be=20exploited=0A+=20=20=20=20= =20=20=20silently.=20Access=20to=20the=20database=20must=20be=20tightly=20= restricted=20to=20only=0A+=20=20=20=20=20=20=20trusted=20clients=0A+=20=20= =20=20=20=20=20=0A+=20=20=20=20=20=20=20=20=0A+=20=20=20=20= =20=20=20=20=20That=20is,=20"trusted"=20in=20the=20sense=20that=20the=20= OAuth=20client=20and=20the=0A+=20=20=20=20=20=20=20=20=20= PostgreSQL=20server=20are=20controlled=20by=20= the=20same=0A+=20=20=20=20=20=20=20=20=20entity.=20Notably,=20the=20= Device=20Authorization=20client=20flow=20supported=20by=0A+=20=20=20=20=20= =20=20=20=20libpq=20does=20not=20usually=20meet=20this=20bar,=20since=20= it's=20designed=20for=20use=20by=0A+=20=20=20=20=20=20=20=20=20= public/untrusted=20clients.=0A+=20=20=20=20=20=20=20=20=0A+=20=20=20= =20=20=20=20=0A+=20=20=20=20=20=20=20if=20users=20are=20not=20= prompted=20for=20additional=20scopes.=0A+=20=20=20=20=20=20=0A+=20= =20=20=20=20=0A+=20=20=20=20=0A+=20=20=20=20= =0A+=20=20=20=20=20Authenticate=20the=20End=20= User=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20=0A+=20= =20=20=20=20=20=20Finally,=20the=20validator=20should=20determine=20a=20= user=20identifier=20for=20the=20token,=0A+=20=20=20=20=20=20=20either=20= by=20asking=20the=20provider=20for=20this=20information=20or=20by=20= extracting=20it=0A+=20=20=20=20=20=20=20from=20the=20token=20itself,=20= and=20return=20that=20identifier=20to=20the=20server=20(which=0A+=20=20=20= =20=20=20=20will=20then=20make=20a=20final=20authorization=20decision=20= using=20the=20HBA=0A+=20=20=20=20=20=20=20configuration).=20This=20= identifier=20will=20be=20available=20within=20the=20session=20via=0A+=20=20= =20=20=20=20=20system_user<= /link>=0A+=20=20=20=20=20=20=20and=20recorded=20in=20the=20server=20logs=20= if=20=0A+=20=20=20=20=20=20=20= is=20enabled.=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=20Different=20providers=20may=20record=20a=20variety=20= of=20different=20authentication=0A+=20=20=20=20=20=20=20information=20= for=20an=20end=20user,=20typically=20referred=20to=20as=0A+=20=20=20=20=20= =20=20claims.=20Providers=20usually=20document=20= which=20of=20these=0A+=20=20=20=20=20=20=20claims=20are=20trustworthy=20= enough=20to=20use=20for=20authorization=20decisions=20and=0A+=20=20=20=20= =20=20=20which=20are=20not.=20(For=20instance,=20it=20would=20probably=20= not=20be=20wise=20to=20use=20an=0A+=20=20=20=20=20=20=20end=20user's=20= full=20name=20as=20the=20identifier=20for=20authentication,=20since=20= many=0A+=20=20=20=20=20=20=20providers=20allow=20users=20to=20change=20= their=20display=20names=20arbitrarily.)=0A+=20=20=20=20=20=20=20= Ultimately,=20the=20choice=20of=20which=20claim=20(or=20combination=20of=20= claims)=20to=20use=0A+=20=20=20=20=20=20=20comes=20down=20to=20the=20= provider=20implementation=20and=20application=20requirements.=0A+=20=20=20= =20=20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20= Note=20that=20anonymous/pseudonymous=20login=20is=20possible=20as=20= well,=20by=20enabling=0A+=20=20=20=20=20=20=20usermap=20delegation;=20= see=0A+=20=20=20=20=20=20=20.=0A+=20=20=20=20=20= =20=0A+=20=20=20=20=20=0A+=20=20=20=20=0A= +=20=20=20=0A+=20=20=0A+=0A+=20=20=0A+=20=20=20General=20= Coding=20Guidelines=0A+=20=20=20=0A+=20=20=20=20Developers=20= should=20keep=20the=20following=20in=20mind=20when=20implementing=20= token=0A+=20=20=20=20validation:=0A+=20=20=20=0A+=20=20=20= =0A+=20=20=20=20=0A+=20=20=20=20=20= Token=20Confidentiality=0A+=20=20=20=20=20=0A+=20=20= =20=20=20=20=0A+=20=20=20=20=20=20=20Modules=20should=20not=20= write=20tokens,=20or=20pieces=20of=20tokens,=20into=20the=20server=0A+=20= =20=20=20=20=20=20log.=20This=20is=20true=20even=20if=20the=20module=20= considers=20the=20token=20invalid;=20an=0A+=20=20=20=20=20=20=20attacker=20= who=20confuses=20a=20client=20into=20communicating=20with=20the=20wrong=20= provider=0A+=20=20=20=20=20=20=20should=20not=20be=20able=20to=20= retrieve=20that=20(otherwise=20valid)=20token=20from=20the=0A+=20=20=20=20= =20=20=20disk.=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=20Implementations=20that=20send=20tokens=20over=20= the=20network=20(for=20example,=20to=0A+=20=20=20=20=20=20=20perform=20= online=20token=20validation=20with=20a=20provider)=20must=20authenticate=20= the=0A+=20=20=20=20=20=20=20peer=20and=20ensure=20that=20strong=20= transport=20security=20is=20in=20use.=0A+=20=20=20=20=20=20=0A+=20= =20=20=20=20=0A+=20=20=20=20=0A+=20=20=20=20= =0A+=20=20=20=20=20Logging=0A+=20=20=20=20=20= =0A+=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20Modules=20= may=20use=20the=20same=20logging=0A+=20=20=20=20=20=20=20= facilities=20as=20standard=20extensions;=20however,=20the=20rules=20= for=20emitting=0A+=20=20=20=20=20=20=20log=20entries=20to=20the=20client=20= are=20subtly=20different=20during=20the=20authentication=0A+=20=20=20=20=20= =20=20phase=20of=20the=20connection.=20Generally=20speaking,=20modules=20= should=20log=0A+=20=20=20=20=20=20=20verification=20problems=20at=20the=20= COMMERROR=20level=20and=20return=0A+=20=20=20=20=20=20=20= normally,=20instead=20of=20using=20= ERROR/FATAL=0A+=20=20=20=20=20=20=20to=20= unwind=20the=20stack,=20to=20avoid=20leaking=20information=20to=20= unauthenticated=0A+=20=20=20=20=20=20=20clients.=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=0A+=20=20=20=20=0A+=20= =20=20=20=0A+=20=20=20=20=20Interruptibility=0A= +=20=20=20=20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =20=20Modules=20must=20remain=20interruptible=20by=20signals=20so=20that=20= the=20server=20can=0A+=20=20=20=20=20=20=20correctly=20handle=20= authentication=20timeouts=20and=20shutdown=20signals=20from=0A+=20=20=20=20= =20=20=20pg_ctl.=20For=20example,=20a=20= module=20receiving=0A+=20=20=20=20=20=20=20= EINTR/EAGAIN=20from=20a=20blocking=20= call=0A+=20=20=20=20=20=20=20should=20call=20= CHECK_FOR_INTERRUPTS()=20before=20retrying.=0A+=20=20= =20=20=20=20=20The=20same=20should=20be=20done=20during=20any=20= long-running=20loops.=20Failure=20to=20follow=0A+=20=20=20=20=20=20=20= this=20guidance=20may=20result=20in=20unresponsive=20backend=20sessions.=0A= +=20=20=20=20=20=20=0A+=20=20=20=20=20=0A+=20=20=20=20= =0A+=20=20=20=20=0A+=20=20=20=20=20= Testing=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20The=20breadth=20of=20testing=20an=20OAuth=20= system=20is=20well=20beyond=20the=20scope=20of=20this=0A+=20=20=20=20=20=20= =20documentation,=20but=20at=20minimum,=20negative=20testing=20should=20= be=20considered=0A+=20=20=20=20=20=20=20mandatory.=20It's=20trivial=20to=20= design=20a=20module=20that=20lets=20authorized=20users=20in;=0A+=20=20=20= =20=20=20=20the=20whole=20point=20of=20the=20system=20is=20to=20keep=20= unauthorized=20users=20out.=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=20=20=20=20=0A+=20=20=20=20=0A= +=20=20=20=20=20Documentation=0A+=20=20=20=20=20=0A= +=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20Validator=20= implementations=20should=20document=20the=20contents=20and=20format=20of=20= the=0A+=20=20=20=20=20=20=20authenticated=20ID=20that=20is=20reported=20= to=20the=20server=20for=20each=20end=20user,=20since=0A+=20=20=20=20=20=20= =20DBAs=20may=20need=20to=20use=20this=20information=20to=20construct=20= pg_ident=20maps.=20(For=0A+=20=20=20=20=20=20=20instance,=20is=20it=20an=20= email=20address?=20an=20organizational=20ID=20number?=20a=20UUID?)=0A+=20= =20=20=20=20=20=20They=20should=20also=20document=20whether=20or=20not=20= it=20is=20safe=20to=20use=20the=20module=20in=0A+=20=20=20=20=20=20=20= delegate_ident_mapping=3D1=20mode,=20and=20what=20= additional=0A+=20=20=20=20=20=20=20configuration=20is=20required=20in=20= order=20to=20do=20so.=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=20=20=20=20=0A+=20=20=20=0A= +=20=20=0A+=0A+=20=20=0A+=20=20=20= Authorizing=20Users=20(Usermap=20Delegation)=0A+=20=20=20= =0A+=20=20=20=20The=20standard=20deliverable=20of=20a=20validation=20= module=20is=20the=20user=20identifier,=0A+=20=20=20=20which=20the=20= server=20will=20then=20compare=20to=20any=20configured=0A+=20=20=20=20= pg_ident.conf=0A= +=20=20=20=20mappings=20and=20determine=20whether=20the=20end=20= user=20is=20authorized=20to=20connect.=0A+=20=20=20=20However,=20OAuth=20= is=20itself=20an=20authorization=20framework,=20and=20tokens=20may=20= carry=0A+=20=20=20=20information=20about=20user=20privileges.=20For=20= example,=20a=20token=20may=20be=20associated=0A+=20=20=20=20with=20the=20= organizational=20groups=20that=20a=20user=20belongs=20to,=20or=20list=20= the=20roles=0A+=20=20=20=20that=20a=20user=20may=20assume,=20and=20= duplicating=20that=20knowledge=20into=20local=20usermaps=0A+=20=20=20=20= for=20every=20server=20may=20not=20be=20desirable.=0A+=20=20=20=0A= +=20=20=20=0A+=20=20=20=20To=20bypass=20username=20mapping=20= entirely,=20and=20have=20the=20validator=20module=20assume=0A+=20=20=20=20= the=20additional=20responsibility=20of=20authorizing=20user=20= connections,=20the=20HBA=20may=0A+=20=20=20=20be=20configured=20with=20= .=0A+=20=20=20=20= The=20module=20may=20then=20use=20token=20scopes=20or=20an=20equivalent=20= method=20to=20decide=0A+=20=20=20=20whether=20the=20user=20is=20allowed=20= to=20connect=20under=20their=20desired=20role.=20The=20user=0A+=20=20=20=20= identifier=20will=20still=20be=20recorded=20by=20the=20server,=20but=20= it=20plays=20no=20part=20in=0A+=20=20=20=20determining=20whether=20to=20= continue=20the=20connection.=0A+=20=20=20=0A+=20=20=20=0A+=20= =20=20=20Using=20this=20scheme,=20authentication=20itself=20is=20= optional.=20As=20long=20as=20the=20module=0A+=20=20=20=20reports=20that=20= the=20connection=20is=20authorized,=20login=20will=20continue=20even=20= if=20there=0A+=20=20=20=20is=20no=20recorded=20user=20identifier=20at=20= all.=20This=20makes=20it=20possible=20to=20implement=0A+=20=20=20=20= anonymous=20or=20pseudonymous=20access=20to=20the=20database,=20where=20= the=20third-party=0A+=20=20=20=20provider=20performs=20all=20necessary=20= authentication=20but=20does=20not=20provide=20any=0A+=20=20=20=20= user-identifying=20information=20to=20the=20server.=20(Some=20providers=20= may=20create=20an=0A+=20=20=20=20anonymized=20ID=20number=20that=20can=20= be=20recorded=20instead,=20for=20later=20auditing.)=0A+=20=20=20=0A= +=20=20=20=0A+=20=20=20=20Usermap=20delegation=20provides=20the=20= most=20architectural=20flexibility,=20but=20it=20turns=0A+=20=20=20=20= the=20validator=20module=20into=20a=20single=20point=20of=20failure=20= for=20connection=0A+=20=20=20=20authorization.=20Use=20with=20caution.=0A= +=20=20=20=0A+=20=20=0A+=20=0A+=0A+=20=0A+=20=20Initialization=20= Functions=0A+=20=20=0A= +=20=20=20_PG_oauth_validator_module_init=0A+=20=20= =0A+=20=20=0A+=20=20=20An=20OAuth=20validator=20module=20= is=20loaded=20by=20dynamically=20loading=20one=20of=20the=20shared=0A+=20= =20=20libraries=20listed=20in=20.=0A+=20=20=20The=20normal=20= library=20search=20path=20is=20used=20to=20locate=20the=20library.=20To=0A= +=20=20=20provide=20the=20validator=20callbacks=20and=20to=20indicate=20= that=20the=20library=20is=20an=20OAuth=0A+=20=20=20validator=20module=20= a=20function=20named=0A+=20=20=20= _PG_oauth_validator_module_init=20must=20be=20= provided.=20The=0A+=20=20=20return=20value=20of=20the=20function=20must=20= be=20a=20pointer=20to=20a=20struct=20of=20type=0A+=20=20=20= OAuthValidatorCallbacks,=20which=20contains=20= pointers=20to=0A+=20=20=20the=20module's=20token=20validation=20= functions.=20The=20returned=0A+=20=20=20pointer=20must=20be=20of=20= server=20lifetime,=20which=20is=20typically=20achieved=20by=20defining=0A= +=20=20=20it=20as=20a=20static=20const=20variable=20= in=20global=20scope.=0A+=0A+typedef=20struct=20= OAuthValidatorCallbacks=0A+{=0A+=20=20=20=20ValidatorStartupCB=20= startup_cb;=0A+=20=20=20=20ValidatorShutdownCB=20shutdown_cb;=0A+=20=20=20= =20ValidatorValidateCB=20validate_cb;=0A+}=20OAuthValidatorCallbacks;=0A= +=0A+typedef=20const=20OAuthValidatorCallbacks=20= *(*OAuthValidatorModuleInit)=20(void);=0A+=0A+=0A+=20=20= =20Only=20the=20validate_cb=20callback=20is=20= required,=20the=20others=0A+=20=20=20are=20optional.=0A+=20=20=0A= +=20=0A+=0A+=20=0A+=20=20= OAuth=20Validator=20Callbacks=0A+=20=20=0A+=20=20=20= OAuth=20validator=20modules=20implement=20their=20functionality=20by=20= defining=20a=20set=20of=0A+=20=20=20callbacks.=20The=20server=20will=20= call=20them=20as=20required=20to=20process=20the=0A+=20=20=20= authentication=20request=20from=20the=20user.=0A+=20=20=0A+=0A+=20= =20=0A+=20=20=20= Startup=20Callback=0A+=20=20=20=0A+=20=20=20=20The=20= startup_cb=20callback=20is=20executed=20directly=20= after=0A+=20=20=20=20loading=20the=20module.=20This=20callback=20can=20= be=20used=20to=20set=20up=20local=20state=20and=0A+=20=20=20=20perform=20= additional=20initialization=20if=20required.=20If=20the=20validator=20= module=0A+=20=20=20=20has=20state=20it=20can=20use=20= state->private_data=20to=0A+=20=20=20=20store=20= it.=0A+=0A+=0A+typedef=20void=20(*ValidatorStartupCB)=20= (ValidatorModuleState=20*state);=0A+=0A+=20=20=20= =0A+=20=20=0A+=0A+=20=20=0A+=20=20=20Validate=20= Callback=0A+=20=20=20=0A+=20=20=20=20The=20= validate_cb=20callback=20is=20executed=20during=20= the=20OAuth=0A+=20=20=20=20exchange=20when=20a=20user=20attempts=20to=20= authenticate=20using=20OAuth.=20=20Any=20state=20set=20in=0A+=20=20=20=20= previous=20calls=20will=20be=20available=20in=20= state->private_data.=0A+=0A+=0A= +typedef=20ValidatorModuleResult=20*(*ValidatorValidateCB)=20= (ValidatorModuleState=20*state,=20const=20char=20*token,=20const=20char=20= *role);=0A+=0A+=0A+=20=20=20=20= token=20will=20contain=20the=20bearer=20token=20= to=20validate.=0A+=20=20=20=20The=20server=20has=20ensured=20that=20the=20= token=20is=20well-formed=20syntactically,=20but=20no=0A+=20=20=20=20= other=20validation=20has=20been=20performed.=20=20= role=20will=0A+=20=20=20=20contain=20the=20= role=20the=20user=20has=20requested=20to=20log=20in=20as.=20=20The=20= callback=20must=0A+=20=20=20=20return=20a=20palloc'd=20= ValidatorModuleResult=20struct,=20which=20is=0A+=20=20= =20=20defined=20as=20below:=0A+=0A+=0A+typedef=20struct=20= ValidatorModuleResult=0A+{=0A+=20=20=20=20bool=20=20=20=20=20=20=20=20= authorized;=0A+=20=20=20=20char=20=20=20=20=20=20=20*authn_id;=0A+}=20= ValidatorModuleResult;=0A+=0A+=0A+=20=20=20=20The=20= connection=20will=20only=20proceed=20if=20the=20module=20sets=0A+=20=20=20= =20authorized=20to=20true.=20= =20To=0A+=20=20=20=20authenticate=20the=20user,=20the=20authenticated=20= user=20name=20(as=20determined=20using=20the=0A+=20=20=20=20token)=20= shall=20be=20palloc'd=20and=20returned=20in=20the=20= authn_id=0A+=20=20=20=20field.=20=20= Alternatively,=20authn_id=20may=20be=20set=20= to=0A+=20=20=20=20NULL=20if=20the=20token=20is=20valid=20but=20the=20= associated=20user=20identity=20cannot=20be=0A+=20=20=20=20determined.=0A= +=20=20=20=0A+=20=20=20=0A+=20=20=20=20The=20caller=20= assumes=20ownership=20of=20the=20returned=20memory=20allocation,=20the=0A= +=20=20=20=20validator=20module=20should=20not=20in=20any=20way=20access=20= the=20memory=20after=20it=20has=20been=0A+=20=20=20=20returned.=20=20A=20= validator=20may=20instead=20return=20NULL=20to=20signal=20an=20internal=0A= +=20=20=20=20error.=0A+=20=20=20=0A+=20=20=20=0A+=20=20=20=20= The=20behavior=20after=20validate_cb=20returns=20= depends=20on=20the=0A+=20=20=20=20specific=20HBA=20setup.=20=20Normally,=20= the=20authn_id=20user=0A+=20=20=20=20name=20= must=20exactly=20match=20the=20role=20that=20the=20user=20is=20logging=20= in=20as.=20=20(This=0A+=20=20=20=20behavior=20may=20be=20modified=20with=20= a=20usermap.)=20=20But=20when=20authenticating=20against=0A+=20=20=20=20= an=20HBA=20rule=20with=20trust_validator_authz=20= turned=20on,=20the=0A+=20=20=20=20server=20will=20not=20perform=20any=20= checks=20on=20the=20value=20of=0A+=20=20=20=20= authn_id=20at=20all;=20in=20this=20case=20it=20= is=20up=20to=20the=0A+=20=20=20=20validator=20to=20ensure=20that=20the=20= token=20carries=20enough=20privileges=20for=20the=20user=20to=0A+=20=20=20= =20log=20in=20under=20the=20indicated=20role.=0A= +=20=20=20=0A+=20=20=0A+=0A+=20=20=0A+=20=20=20Shutdown=20= Callback=0A+=20=20=20=0A+=20=20=20=20The=20= shutdown_cb=20callback=20is=20executed=20when=20the=20= backend=0A+=20=20=20=20process=20associated=20with=20the=20connection=20= exits.=20If=20the=20validator=20module=20has=0A+=20=20=20=20any=20state,=20= this=20callback=20should=20free=20it=20to=20avoid=20resource=20leaks.=0A= +=0A+typedef=20void=20(*ValidatorShutdownCB)=20= (ValidatorModuleState=20*state);=0A+=0A+=20=20=20= =0A+=20=20=0A+=0A+=20=0A+=0Adiff=20= --git=20a/doc/src/sgml/postgres.sgml=20b/doc/src/sgml/postgres.sgml=0A= index=207be25c58507..af476c82fcc=20100644=0A---=20= a/doc/src/sgml/postgres.sgml=0A+++=20b/doc/src/sgml/postgres.sgml=0A@@=20= -229,6=20+229,7=20@@=20break=20is=20not=20needed=20in=20a=20wider=20= output=20rendering.=0A=20=20=20&logicaldecoding;=0A=20=20=20= &replication-origins;=0A=20=20=20&archive-modules;=0A+=20=20= &oauth-validators;=0A=20=0A=20=20=0A=20=0Adiff=20--git=20= a/doc/src/sgml/protocol.sgml=20b/doc/src/sgml/protocol.sgml=0Aindex=20= fb5dec1172e..3bd9e68e6ce=20100644=0A---=20a/doc/src/sgml/protocol.sgml=0A= +++=20b/doc/src/sgml/protocol.sgml=0A@@=20-1688,11=20+1688,11=20@@=20= SELCT=201/0;=0A=20=0A=20=20=20= =0A=20=20=20=20SASL=20is=20a=20framework=20= for=20authentication=20in=20connection-oriented=0A-=20=20=20protocols.=20= At=20the=20moment,=20PostgreSQL=20implements=20= two=20SASL=0A-=20=20=20authentication=20mechanisms,=20SCRAM-SHA-256=20= and=20SCRAM-SHA-256-PLUS.=20More=0A-=20=20=20might=20be=20added=20in=20= the=20future.=20The=20below=20steps=20illustrate=20how=20SASL=0A-=20=20=20= authentication=20is=20performed=20in=20general,=20while=20the=20next=20= subsection=20gives=0A-=20=20=20more=20details=20on=20SCRAM-SHA-256=20and=20= SCRAM-SHA-256-PLUS.=0A+=20=20=20protocols.=20At=20the=20moment,=20= PostgreSQL=20implements=20three=0A+=20=20=20= SASL=20authentication=20mechanisms:=20SCRAM-SHA-256,=20= SCRAM-SHA-256-PLUS,=20and=0A+=20=20=20OAUTHBEARER.=20More=20might=20be=20= added=20in=20the=20future.=20The=20below=20steps=20illustrate=20how=20= SASL=0A+=20=20=20authentication=20is=20performed=20in=20general,=20while=20= the=20next=20subsections=20give=0A+=20=20=20more=20details=20on=20= particular=20mechanisms.=0A=20=20=20=0A=20=0A=20=20=20=0A= @@=20-1727,7=20+1727,7=20@@=20SELCT=201/0;=0A=20=20=20=20=0A=20=20=20= =20=20=0A=20=20=20=20=20=20Finally,=20when=20the=20authentication=20= exchange=20is=20completed=20successfully,=20the=0A-=20=20=20=20=20server=20= sends=20an=20AuthenticationSASLFinal=20message,=20followed=0A+=20=20=20=20= =20server=20sends=20an=20optional=20AuthenticationSASLFinal=20message,=20= followed=0A=20=20=20=20=20=20immediately=20by=20an=20AuthenticationOk=20= message.=20The=20AuthenticationSASLFinal=0A=20=20=20=20=20=20contains=20= additional=20server-to-client=20data,=20whose=20content=20is=20= particular=20to=20the=0A=20=20=20=20=20=20selected=20authentication=20= mechanism.=20If=20the=20authentication=20mechanism=20doesn't=0A@@=20= -1746,9=20+1746,9=20@@=20SELCT=201/0;=0A=20=20=20=20SCRAM-SHA-256=20= Authentication=0A=20=0A=20=20=20=20=0A-=20=20=20=20The=20= implemented=20SASL=20mechanisms=20at=20the=20moment=0A-=20=20=20=20are=20= SCRAM-SHA-256=20and=20its=20variant=20with=20channel=0A= -=20=20=20=20binding=20SCRAM-SHA-256-PLUS.=20They=20= are=20described=20in=0A+=20=20=20=20SCRAM-SHA-256,=20= and=20its=20variant=20with=20channel=0A+=20=20=20=20binding=20= SCRAM-SHA-256-PLUS,=20are=20password-based=0A+=20=20=20= =20authentication=20mechanisms.=20They=20are=20described=20in=0A=20=20=20= =20=20detail=20in=20RFC=207677=0A= =20=20=20=20=20and=20RFC=20= 5802.=0A=20=20=20=20=0A@@=20-1850,6=20+1850,121=20@@=20= SELCT=201/0;=0A=20=20=20=20=20= =0A=20=20=20=20=0A=20=20=20=0A+=0A+=20=20= =0A+=20=20=20OAUTHBEARER=20= Authentication=0A+=0A+=20=20=20=0A+=20=20=20=20= OAUTHBEARER=20is=20a=20token-based=20mechanism=20for=20= federated=0A+=20=20=20=20authentication.=20It=20is=20described=20in=20= detail=20in=0A+=20=20=20=20RFC=20= 7628.=0A+=20=20=20=0A+=0A+=20=20=20=0A+=20=20=20=20= A=20typical=20exchange=20differs=20depending=20on=20whether=20or=20not=20= the=20client=20already=0A+=20=20=20=20has=20a=20bearer=20token=20cached=20= for=20the=20current=20user.=20If=20it=20does=20not,=20the=20exchange=0A+=20= =20=20=20will=20take=20place=20over=20two=20connections:=20the=20first=20= "discovery"=20connection=20to=0A+=20=20=20=20obtain=20OAuth=20metadata=20= from=20the=20server,=20and=20the=20second=20connection=20to=20send=0A+=20= =20=20=20the=20token=20after=20the=20client=20has=20obtained=20it.=20= (libpq=20does=20not=20currently=0A+=20=20=20=20implement=20a=20caching=20= method=20as=20part=20of=20its=20builtin=20flow,=20so=20it=20uses=20the=0A= +=20=20=20=20two-connection=20exchange.)=0A+=20=20=20=0A+=0A+=20=20= =20=0A+=20=20=20=20This=20mechanism=20is=20client-initiated,=20= like=20SCRAM.=20The=20client=20initial=20response=0A+=20=20=20=20= consists=20of=20the=20standard=20"GS2"=20header=20used=20by=20SCRAM,=20= followed=20by=20a=20list=20of=0A+=20=20=20=20= key=3Dvalue=20pairs.=20The=20only=20key=20currently=20= supported=20by=0A+=20=20=20=20the=20server=20is=20= auth,=20which=20contains=20the=20bearer=20token.=0A+=20= =20=20=20OAUTHBEARER=20additionally=20specifies=20= three=20optional=0A+=20=20=20=20components=20of=20the=20client=20initial=20= response=20(the=20authzid=20of=0A+=20=20=20=20the=20= GS2=20header,=20and=20the=20host=20and=0A+=20=20= =20=20port=20keys)=20which=20are=20currently=20= ignored=20by=20the=0A+=20=20=20=20server.=0A+=20=20=20=0A+=0A+=20=20= =20=0A+=20=20=20=20OAUTHBEARER=20does=20not=20= support=20channel=20binding,=20and=20there=0A+=20=20=20=20is=20no=20= "OAUTHBEARER-PLUS"=20mechanism.=20This=20mechanism=20does=20not=20make=20= use=20of=0A+=20=20=20=20server=20data=20during=20a=20successful=20= authentication,=20so=20the=0A+=20=20=20=20AuthenticationSASLFinal=20= message=20is=20not=20used=20in=20the=20exchange.=0A+=20=20=20=0A+=0A= +=20=20=20=0A+=20=20=20=20Example=0A+=20=20=20=20= =0A+=20=20=20=20=20=0A+=20=20=20=20=20=20During=20the=20= first=20exchange,=20the=20server=20sends=20an=20AuthenticationSASL=20= message=0A+=20=20=20=20=20=20with=20the=20OAUTHBEARER=20= mechanism=20advertised.=0A+=20=20=20=20=20=0A+=20=20=20=20=0A= +=0A+=20=20=20=20=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20= The=20client=20responds=20by=20sending=20a=20SASLInitialResponse=20= message=20which=0A+=20=20=20=20=20=20indicates=20the=20= OAUTHBEARER=20mechanism.=20Assuming=20the=0A+=20=20=20= =20=20=20client=20does=20not=20already=20have=20a=20valid=20bearer=20= token=20for=20the=20current=20user,=0A+=20=20=20=20=20=20the=20= auth=20field=20is=20empty,=20indicating=20a=20= discovery=0A+=20=20=20=20=20=20connection.=0A+=20=20=20=20=20=0A+=20= =20=20=20=0A+=0A+=20=20=20=20=0A+=20=20=20=20=20=0A+=20= =20=20=20=20=20Server=20sends=20an=20AuthenticationSASLContinue=20= message=20containing=20an=20error=0A+=20=20=20=20=20=20= status=20alongside=20a=20well-known=20URI=20and=20= scopes=20that=20the=0A+=20=20=20=20=20=20client=20should=20use=20to=20= conduct=20an=20OAuth=20flow.=0A+=20=20=20=20=20=0A+=20=20=20=20= =0A+=0A+=20=20=20=20=0A+=20=20=20=20=20=0A+=20=20=20=20= =20=20Client=20sends=20a=20SASLResponse=20message=20containing=20the=20= empty=20set=20(a=20single=0A+=20=20=20=20=20=200x01=20= byte)=20to=20finish=20its=20half=20of=20the=20discovery=0A+=20=20=20=20=20= =20exchange.=0A+=20=20=20=20=20=0A+=20=20=20=20=0A+=0A+=20=20= =20=20=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20Server=20= sends=20an=20ErrorMessage=20to=20fail=20the=20first=20exchange.=0A+=20=20= =20=20=20=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20At=20this=20= point,=20the=20client=20conducts=20one=20of=20many=20possible=20OAuth=20= flows=20to=0A+=20=20=20=20=20=20obtain=20a=20bearer=20token,=20using=20= any=20metadata=20that=20it=20has=20been=20configured=20with=0A+=20=20=20=20= =20=20in=20addition=20to=20that=20provided=20by=20the=20server.=20(This=20= description=20is=20left=0A+=20=20=20=20=20=20deliberately=20vague;=20= OAUTHBEARER=20does=20not=20specify=20or=0A+=20=20=20=20= =20=20mandate=20any=20particular=20method=20for=20obtaining=20a=20= token.)=0A+=20=20=20=20=20=0A+=20=20=20=20=20=0A+=20=20=20=20= =20=20Once=20it=20has=20a=20token,=20the=20client=20reconnects=20to=20= the=20server=20for=20the=20final=0A+=20=20=20=20=20=20exchange:=0A+=20=20= =20=20=20=0A+=20=20=20=20=0A+=0A+=20=20=20=20=0A+=20=20= =20=20=20=0A+=20=20=20=20=20=20The=20server=20once=20again=20sends=20= an=20AuthenticationSASL=20message=20with=20the=0A+=20=20=20=20=20=20= OAUTHBEARER=20mechanism=20advertised.=0A+=20=20=20=20=20= =0A+=20=20=20=20=0A+=0A+=20=20=20=20=0A+=20=20=20=20=20= =0A+=20=20=20=20=20=20The=20client=20responds=20by=20sending=20a=20= SASLInitialResponse=20message,=20but=20this=0A+=20=20=20=20=20=20time=20= the=20auth=20field=20in=20the=20message=20= contains=20the=0A+=20=20=20=20=20=20bearer=20token=20that=20was=20= obtained=20during=20the=20client=20flow.=0A+=20=20=20=20=20=0A+=20= =20=20=20=0A+=0A+=20=20=20=20=0A+=20=20=20=20=20=0A+=20= =20=20=20=20=20The=20server=20validates=20the=20token=20according=20to=20= the=20instructions=20of=20the=0A+=20=20=20=20=20=20token=20provider.=20= If=20the=20client=20is=20authorized=20to=20connect,=20it=20sends=20an=0A= +=20=20=20=20=20=20AuthenticationOk=20message=20to=20end=20the=20SASL=20= exchange.=0A+=20=20=20=20=20=0A+=20=20=20=20=0A+=20=20=20= =0A+=20=20=0A=20=20=0A=20=0A=20=20=0Adiff=20--git=20= a/doc/src/sgml/regress.sgml=20b/doc/src/sgml/regress.sgml=0Aindex=20= 7c474559bdf..0e5e8e8f309=20100644=0A---=20a/doc/src/sgml/regress.sgml=0A= +++=20b/doc/src/sgml/regress.sgml=0A@@=20-347,6=20+347,16=20@@=20make=20= check-world=20PG_TEST_EXTRA=3D'kerberos=20ldap=20ssl=20load_balance=20= libpq_encryption'=0A=20=20=20=20=20=20=20=0A=20=20=20=20=20=20= =0A=20=20=20=20=20=0A+=0A+=20=20=20=20= =0A+=20=20=20=20=20oauth=0A= +=20=20=20=20=20=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =20=20Runs=20the=20test=20suite=20under=20= src/test/modules/oauth_validator.=0A+=20=20=20=20=20= =20=20This=20opens=20TCP/IP=20listen=20sockets=20for=20a=20test-server=20= running=20HTTPS.=0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=20=20=20=20=0A=20=20=20=20=0A= =20=0A=20=20=20=20Tests=20for=20features=20that=20are=20not=20supported=20= by=20the=20current=20build=0Adiff=20--git=20a/meson.build=20= b/meson.build=0Aindex=207dd7110318d..5c35159b4f1=20100644=0A---=20= a/meson.build=0A+++=20b/meson.build=0A@@=20-855,6=20+855,101=20@@=20= endif=0A=20=0A=20=0A=20=0A= +###############################################################=0A+#=20= Library:=20libcurl=0A= +###############################################################=0A+=0A= +libcurlopt=20=3D=20get_option('libcurl')=0A+if=20not=20= libcurlopt.disabled()=0A+=20=20#=20Check=20for=20libcurl=207.61.0=20or=20= higher=20(corresponding=20to=20RHEL8=20and=20the=20ability=0A+=20=20#=20= to=20explicitly=20set=20TLS=201.3=20ciphersuites).=0A+=20=20libcurl=20=3D=20= dependency('libcurl',=20version:=20'>=3D=207.61.0',=20required:=20= libcurlopt)=0A+=20=20if=20libcurl.found()=0A+=20=20=20=20= cdata.set('USE_LIBCURL',=201)=0A+=0A+=20=20=20=20#=20Check=20to=20see=20= whether=20the=20current=20platform=20supports=20threadsafe=20Curl=0A+=20=20= =20=20#=20initialization.=0A+=20=20=20=20libcurl_threadsafe_init=20=3D=20= false=0A+=0A+=20=20=20=20if=20not=20meson.is_cross_build()=0A+=20=20=20=20= =20=20r=20=3D=20cc.run('''=0A+=20=20=20=20=20=20=20=20#include=20= =0A+=0A+=20=20=20=20=20=20=20=20int=20main(void)=0A+=20=20=20= =20=20=20=20=20{=0A+=20=20=20=20=20=20=20=20=20=20=20=20= curl_version_info_data=20*info;=0A+=0A+=20=20=20=20=20=20=20=20=20=20=20=20= if=20(curl_global_init(CURL_GLOBAL_ALL))=0A+=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20return=20-1;=0A+=0A+=20=20=20=20=20=20=20=20=20=20=20=20= info=20=3D=20curl_version_info(CURLVERSION_NOW);=0A+=20=20=20=20=20=20=20= =20#ifdef=20CURL_VERSION_THREADSAFE=0A+=20=20=20=20=20=20=20=20=20=20=20=20= if=20(info->features=20&=20CURL_VERSION_THREADSAFE)=0A+=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20return=200;=0A+=20=20=20=20=20=20=20=20= #endif=0A+=0A+=20=20=20=20=20=20=20=20=20=20=20=20return=201;=0A+=20=20=20= =20=20=20=20=20}''',=0A+=20=20=20=20=20=20=20=20name:=20'test=20for=20= curl_global_init=20thread=20safety',=0A+=20=20=20=20=20=20=20=20= dependencies:=20libcurl,=0A+=20=20=20=20=20=20)=0A+=0A+=20=20=20=20=20=20= assert(r.compiled())=0A+=20=20=20=20=20=20if=20r.returncode()=20=3D=3D=20= 0=0A+=20=20=20=20=20=20=20=20libcurl_threadsafe_init=20=3D=20true=0A+=20=20= =20=20=20=20=20=20message('curl_global_init=20is=20threadsafe')=0A+=20=20= =20=20=20=20elif=20r.returncode()=20=3D=3D=201=0A+=20=20=20=20=20=20=20=20= message('curl_global_init=20is=20not=20threadsafe')=0A+=20=20=20=20=20=20= else=0A+=20=20=20=20=20=20=20=20message('curl_global_init=20failed;=20= assuming=20not=20threadsafe')=0A+=20=20=20=20=20=20endif=0A+=20=20=20=20= endif=0A+=0A+=20=20=20=20if=20libcurl_threadsafe_init=0A+=20=20=20=20=20=20= cdata.set('HAVE_THREADSAFE_CURL_GLOBAL_INIT',=201)=0A+=20=20=20=20endif=0A= +=0A+=20=20=20=20#=20Warn=20if=20a=20thread-friendly=20DNS=20resolver=20= isn't=20built.=0A+=20=20=20=20libcurl_async_dns=20=3D=20false=0A+=0A+=20=20= =20=20if=20not=20meson.is_cross_build()=0A+=20=20=20=20=20=20r=20=3D=20= cc.run('''=0A+=20=20=20=20=20=20=20=20#include=20=0A+=0A+=20= =20=20=20=20=20=20=20int=20main(void)=0A+=20=20=20=20=20=20=20=20{=0A+=20= =20=20=20=20=20=20=20=20=20=20=20curl_version_info_data=20*info;=0A+=0A+=20= =20=20=20=20=20=20=20=20=20=20=20if=20= (curl_global_init(CURL_GLOBAL_ALL))=0A+=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20return=20-1;=0A+=0A+=20=20=20=20=20=20=20=20=20=20=20=20info=20= =3D=20curl_version_info(CURLVERSION_NOW);=0A+=20=20=20=20=20=20=20=20=20=20= =20=20return=20(info->features=20&=20CURL_VERSION_ASYNCHDNS)=20?=200=20:=20= 1;=0A+=20=20=20=20=20=20=20=20}''',=0A+=20=20=20=20=20=20=20=20name:=20= 'test=20for=20curl=20support=20for=20asynchronous=20DNS',=0A+=20=20=20=20= =20=20=20=20dependencies:=20libcurl,=0A+=20=20=20=20=20=20)=0A+=0A+=20=20= =20=20=20=20assert(r.compiled())=0A+=20=20=20=20=20=20if=20= r.returncode()=20=3D=3D=200=0A+=20=20=20=20=20=20=20=20libcurl_async_dns=20= =3D=20true=0A+=20=20=20=20=20=20endif=0A+=20=20=20=20endif=0A+=0A+=20=20=20= =20if=20not=20libcurl_async_dns=0A+=20=20=20=20=20=20warning('''=0A+***=20= The=20installed=20version=20of=20libcurl=20does=20not=20support=20= asynchronous=20DNS=0A+***=20lookups.=20Connection=20timeouts=20will=20= not=20be=20honored=20during=20DNS=20resolution,=0A+***=20which=20may=20= lead=20to=20hangs=20in=20client=20programs.''')=0A+=20=20=20=20endif=0A+=20= =20endif=0A+=0A+else=0A+=20=20libcurl=20=3D=20not_found_dep=0A+endif=0A+=0A= +=0A+=0A=20= ###############################################################=0A=20#=20= Library:=20libxml=0A=20= ###############################################################=0A@@=20= -3045,6=20+3140,10=20@@=20libpq_deps=20+=3D=20[=0A=20=0A=20=20=20gssapi,=0A= =20=20=20ldap_r,=0A+=20=20#=20XXX=20libcurl=20must=20link=20after=20= libgssapi_krb5=20on=20FreeBSD=20to=20avoid=20segfaults=0A+=20=20#=20= during=20gss_acquire_cred().=20This=20is=20possibly=20related=20to=20= Curl's=20Heimdal=0A+=20=20#=20dependency=20on=20that=20platform?=0A+=20=20= libcurl,=0A=20=20=20libintl,=0A=20=20=20ssl,=0A=20]=0A@@=20-3721,6=20= +3820,7=20@@=20if=20meson.version().version_compare('>=3D0.57')=0A=20=20=20= =20=20=20=20'gss':=20gssapi,=0A=20=20=20=20=20=20=20'icu':=20icu,=0A=20=20= =20=20=20=20=20'ldap':=20ldap,=0A+=20=20=20=20=20=20'libcurl':=20= libcurl,=0A=20=20=20=20=20=20=20'libxml':=20libxml,=0A=20=20=20=20=20=20=20= 'libxslt':=20libxslt,=0A=20=20=20=20=20=20=20'llvm':=20llvm,=0Adiff=20= --git=20a/meson_options.txt=20b/meson_options.txt=0Aindex=20= d9c7ddccbc4..702c4517145=20100644=0A---=20a/meson_options.txt=0A+++=20= b/meson_options.txt=0A@@=20-100,6=20+100,9=20@@=20option('icu',=20type:=20= 'feature',=20value:=20'auto',=0A=20option('ldap',=20type:=20'feature',=20= value:=20'auto',=0A=20=20=20description:=20'LDAP=20support')=0A=20=0A= +option('libcurl',=20type=20:=20'feature',=20value:=20'auto',=0A+=20=20= description:=20'libcurl=20support')=0A+=0A=20option('libedit_preferred',=20= type:=20'boolean',=20value:=20false,=0A=20=20=20description:=20'Prefer=20= BSD=20Libedit=20over=20GNU=20Readline')=0A=20=0Adiff=20--git=20= a/src/Makefile.global.in=20b/src/Makefile.global.in=0Aindex=20= bbe11e75bf0..3b620bac5ac=20100644=0A---=20a/src/Makefile.global.in=0A+++=20= b/src/Makefile.global.in=0A@@=20-190,6=20+190,7=20@@=20with_systemd=09=3D=20= @with_systemd@=0A=20with_gssapi=09=3D=20@with_gssapi@=0A=20= with_krb_srvnam=09=3D=20@with_krb_srvnam@=0A=20with_ldap=09=3D=20= @with_ldap@=0A+with_libcurl=09=3D=20@with_libcurl@=0A=20with_libxml=09=3D=20= @with_libxml@=0A=20with_libxslt=09=3D=20@with_libxslt@=0A=20with_llvm=09= =3D=20@with_llvm@=0Adiff=20--git=20a/src/backend/libpq/Makefile=20= b/src/backend/libpq/Makefile=0Aindex=206d385fd6a45..98eb2a8242d=20100644=0A= ---=20a/src/backend/libpq/Makefile=0A+++=20b/src/backend/libpq/Makefile=0A= @@=20-15,6=20+15,7=20@@=20include=20$(top_builddir)/src/Makefile.global=0A= =20#=20be-fsstubs=20is=20here=20for=20historical=20reasons,=20probably=20= belongs=20elsewhere=0A=20=0A=20OBJS=20=3D=20\=0A+=09auth-oauth.o=20\=0A=20= =09auth-sasl.o=20\=0A=20=09auth-scram.o=20\=0A=20=09auth.o=20\=0Adiff=20= --git=20a/src/backend/libpq/auth-oauth.c=20= b/src/backend/libpq/auth-oauth.c=0Anew=20file=20mode=20100644=0Aindex=20= 00000000000..aa16977c643=0A---=20/dev/null=0A+++=20= b/src/backend/libpq/auth-oauth.c=0A@@=20-0,0=20+1,864=20@@=0A= +/*-----------------------------------------------------------------------= --=0A+=20*=0A+=20*=20auth-oauth.c=0A+=20*=09=20=20Server-side=20= implementation=20of=20the=20SASL=20OAUTHBEARER=20mechanism.=0A+=20*=0A+=20= *=20See=20the=20following=20RFC=20for=20more=20details:=0A+=20*=20-=20= RFC=207628:=20https://tools.ietf.org/html/rfc7628=0A+=20*=0A+=20*=20= Portions=20Copyright=20(c)=201996-2024,=20PostgreSQL=20Global=20= Development=20Group=0A+=20*=20Portions=20Copyright=20(c)=201994,=20= Regents=20of=20the=20University=20of=20California=0A+=20*=0A+=20*=20= src/backend/libpq/auth-oauth.c=0A+=20*=0A+=20= *-------------------------------------------------------------------------= =0A+=20*/=0A+#include=20"postgres.h"=0A+=0A+#include=20=0A= +#include=20=0A+=0A+#include=20"common/oauth-common.h"=0A= +#include=20"fmgr.h"=0A+#include=20"lib/stringinfo.h"=0A+#include=20= "libpq/auth.h"=0A+#include=20"libpq/hba.h"=0A+#include=20"libpq/oauth.h"=0A= +#include=20"libpq/sasl.h"=0A+#include=20"storage/fd.h"=0A+#include=20= "storage/ipc.h"=0A+#include=20"utils/json.h"=0A+#include=20= "utils/varlena.h"=0A+=0A+/*=20GUC=20*/=0A+char=09=20=20=20= *oauth_validator_libraries_string=20=3D=20NULL;=0A+=0A+static=20void=20= oauth_get_mechanisms(Port=20*port,=20StringInfo=20buf);=0A+static=20void=20= *oauth_init(Port=20*port,=20const=20char=20*selected_mech,=20const=20= char=20*shadow_pass);=0A+static=20int=09oauth_exchange(void=20*opaq,=20= const=20char=20*input,=20int=20inputlen,=0A+=09=09=09=09=09=09=20=20=20= char=20**output,=20int=20*outputlen,=20const=20char=20**logdetail);=0A+=0A= +static=20void=20load_validator_library(const=20char=20*libname);=0A= +static=20void=20shutdown_validator_library(void=20*arg);=0A+=0A+static=20= ValidatorModuleState=20*validator_module_state;=0A+static=20const=20= OAuthValidatorCallbacks=20*ValidatorCallbacks;=0A+=0A+/*=20Mechanism=20= declaration=20*/=0A+const=20pg_be_sasl_mech=20pg_be_oauth_mech=20=3D=20{=0A= +=09.get_mechanisms=20=3D=20oauth_get_mechanisms,=0A+=09.init=20=3D=20= oauth_init,=0A+=09.exchange=20=3D=20oauth_exchange,=0A+=0A+=09= .max_message_length=20=3D=20PG_MAX_AUTH_TOKEN_LENGTH,=0A+};=0A+=0A+/*=20= Valid=20states=20for=20the=20oauth_exchange()=20machine.=20*/=0A+enum=20= oauth_state=0A+{=0A+=09OAUTH_STATE_INIT=20=3D=200,=0A+=09= OAUTH_STATE_ERROR,=0A+=09OAUTH_STATE_FINISHED,=0A+};=0A+=0A+/*=20= Mechanism=20callback=20state.=20*/=0A+struct=20oauth_ctx=0A+{=0A+=09enum=20= oauth_state=20state;=0A+=09Port=09=20=20=20*port;=0A+=09const=20char=20= *issuer;=0A+=09const=20char=20*scope;=0A+};=0A+=0A+static=20char=20= *sanitize_char(char=20c);=0A+static=20char=20= *parse_kvpairs_for_auth(char=20**input);=0A+static=20void=20= generate_error_response(struct=20oauth_ctx=20*ctx,=20char=20**output,=20= int=20*outputlen);=0A+static=20bool=20validate(Port=20*port,=20const=20= char=20*auth);=0A+=0A+/*=20Constants=20seen=20in=20an=20OAUTHBEARER=20= client=20initial=20response.=20*/=0A+#define=20KVSEP=200x01=09=09=09=09= /*=20separator=20byte=20for=20key/value=20pairs=20*/=0A+#define=20= AUTH_KEY=20"auth"=09=09=09/*=20key=20containing=20the=20Authorization=20= header=20*/=0A+#define=20BEARER_SCHEME=20"Bearer=20"=20/*=20required=20= header=20scheme=20(case-insensitive!)=20*/=0A+=0A+/*=0A+=20*=20Retrieves=20= the=20OAUTHBEARER=20mechanism=20list=20(currently=20a=20single=20item).=0A= +=20*=0A+=20*=20For=20a=20full=20description=20of=20the=20API,=20see=20= libpq/sasl.h.=0A+=20*/=0A+static=20void=0A+oauth_get_mechanisms(Port=20= *port,=20StringInfo=20buf)=0A+{=0A+=09/*=20Only=20OAUTHBEARER=20is=20= supported.=20*/=0A+=09appendStringInfoString(buf,=20OAUTHBEARER_NAME);=0A= +=09appendStringInfoChar(buf,=20'\0');=0A+}=0A+=0A+/*=0A+=20*=20= Initializes=20mechanism=20state=20and=20loads=20the=20configured=20= validator=20module.=0A+=20*=0A+=20*=20For=20a=20full=20description=20of=20= the=20API,=20see=20libpq/sasl.h.=0A+=20*/=0A+static=20void=20*=0A= +oauth_init(Port=20*port,=20const=20char=20*selected_mech,=20const=20= char=20*shadow_pass)=0A+{=0A+=09struct=20oauth_ctx=20*ctx;=0A+=0A+=09if=20= (strcmp(selected_mech,=20OAUTHBEARER_NAME)=20!=3D=200)=0A+=09=09= ereport(ERROR,=0A+=09=09=09=09errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09= =09=09=09errmsg("client=20selected=20an=20invalid=20SASL=20= authentication=20mechanism"));=0A+=0A+=09ctx=20=3D=20= palloc0(sizeof(*ctx));=0A+=0A+=09ctx->state=20=3D=20OAUTH_STATE_INIT;=0A= +=09ctx->port=20=3D=20port;=0A+=0A+=09Assert(port->hba);=0A+=09= ctx->issuer=20=3D=20port->hba->oauth_issuer;=0A+=09ctx->scope=20=3D=20= port->hba->oauth_scope;=0A+=0A+=09= load_validator_library(port->hba->oauth_validator);=0A+=0A+=09return=20= ctx;=0A+}=0A+=0A+/*=0A+=20*=20Implements=20the=20OAUTHBEARER=20SASL=20= exchange=20(RFC=207628,=20Sec.=203.2).=20This=20pulls=0A+=20*=20apart=20= the=20client=20initial=20response=20and=20validates=20the=20Bearer=20= token.=20It=20also=0A+=20*=20handles=20the=20dummy=20error=20response=20= for=20a=20failed=20handshake,=20as=20described=20in=0A+=20*=20Sec.=20= 3.2.3.=0A+=20*=0A+=20*=20For=20a=20full=20description=20of=20the=20API,=20= see=20libpq/sasl.h.=0A+=20*/=0A+static=20int=0A+oauth_exchange(void=20= *opaq,=20const=20char=20*input,=20int=20inputlen,=0A+=09=09=09=20=20=20= char=20**output,=20int=20*outputlen,=20const=20char=20**logdetail)=0A+{=0A= +=09char=09=20=20=20*input_copy;=0A+=09char=09=20=20=20*p;=0A+=09char=09=09= cbind_flag;=0A+=09char=09=20=20=20*auth;=0A+=09int=09=09=09status;=0A+=0A= +=09struct=20oauth_ctx=20*ctx=20=3D=20opaq;=0A+=0A+=09*output=20=3D=20= NULL;=0A+=09*outputlen=20=3D=20-1;=0A+=0A+=09/*=0A+=09=20*=20If=20the=20= client=20didn't=20include=20an=20"Initial=20Client=20Response"=20in=20= the=0A+=09=20*=20SASLInitialResponse=20message,=20send=20an=20empty=20= challenge,=20to=20which=20the=0A+=09=20*=20client=20will=20respond=20= with=20the=20same=20data=20that=20usually=20comes=20in=20the=0A+=09=20*=20= Initial=20Client=20Response.=0A+=09=20*/=0A+=09if=20(input=20=3D=3D=20= NULL)=0A+=09{=0A+=09=09Assert(ctx->state=20=3D=3D=20OAUTH_STATE_INIT);=0A= +=0A+=09=09*output=20=3D=20pstrdup("");=0A+=09=09*outputlen=20=3D=200;=0A= +=09=09return=20PG_SASL_EXCHANGE_CONTINUE;=0A+=09}=0A+=0A+=09/*=0A+=09=20= *=20Check=20that=20the=20input=20length=20agrees=20with=20the=20string=20= length=20of=20the=20input.=0A+=09=20*/=0A+=09if=20(inputlen=20=3D=3D=20= 0)=0A+=09=09ereport(ERROR,=0A+=09=09=09=09= errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09=09errmsg("malformed=20= OAUTHBEARER=20message"),=0A+=09=09=09=09errdetail("The=20message=20is=20= empty."));=0A+=09if=20(inputlen=20!=3D=20strlen(input))=0A+=09=09= ereport(ERROR,=0A+=09=09=09=09errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09= =09=09=09errmsg("malformed=20OAUTHBEARER=20message"),=0A+=09=09=09=09= errdetail("Message=20length=20does=20not=20match=20input=20length."));=0A= +=0A+=09switch=20(ctx->state)=0A+=09{=0A+=09=09case=20OAUTH_STATE_INIT:=0A= +=09=09=09/*=20Handle=20this=20case=20below.=20*/=0A+=09=09=09break;=0A+=0A= +=09=09case=20OAUTH_STATE_ERROR:=0A+=0A+=09=09=09/*=0A+=09=09=09=20*=20= Only=20one=20response=20is=20valid=20for=20the=20client=20during=20= authentication=0A+=09=09=09=20*=20failure:=20a=20single=20kvsep.=0A+=09=09= =09=20*/=0A+=09=09=09if=20(inputlen=20!=3D=201=20||=20*input=20!=3D=20= KVSEP)=0A+=09=09=09=09ereport(ERROR,=0A+=09=09=09=09=09=09= errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09=09=09=09= errmsg("malformed=20OAUTHBEARER=20message"),=0A+=09=09=09=09=09=09= errdetail("Client=20did=20not=20send=20a=20kvsep=20response."));=0A+=0A+=09= =09=09/*=20The=20(failed)=20handshake=20is=20now=20complete.=20*/=0A+=09=09= =09ctx->state=20=3D=20OAUTH_STATE_FINISHED;=0A+=09=09=09return=20= PG_SASL_EXCHANGE_FAILURE;=0A+=0A+=09=09default:=0A+=09=09=09elog(ERROR,=20= "invalid=20OAUTHBEARER=20exchange=20state");=0A+=09=09=09return=20= PG_SASL_EXCHANGE_FAILURE;=0A+=09}=0A+=0A+=09/*=20Handle=20the=20client's=20= initial=20message.=20*/=0A+=09p=20=3D=20input_copy=20=3D=20= pstrdup(input);=0A+=0A+=09/*=0A+=09=20*=20OAUTHBEARER=20does=20not=20= currently=20define=20a=20channel=20binding=20(so=20there=20is=20no=0A+=09= =20*=20OAUTHBEARER-PLUS,=20and=20we=20do=20not=20accept=20a=20'p'=20= specifier).=20We=20accept=20a=0A+=09=20*=20'y'=20specifier=20purely=20= for=20the=20remote=20chance=20that=20a=20future=20specification=0A+=09=20= *=20could=20define=20one;=20then=20future=20clients=20can=20still=20= interoperate=20with=20this=0A+=09=20*=20server=20implementation.=20'n'=20= is=20the=20expected=20case.=0A+=09=20*/=0A+=09cbind_flag=20=3D=20*p;=0A+=09= switch=20(cbind_flag)=0A+=09{=0A+=09=09case=20'p':=0A+=09=09=09= ereport(ERROR,=0A+=09=09=09=09=09errcode(ERRCODE_PROTOCOL_VIOLATION),=0A= +=09=09=09=09=09errmsg("malformed=20OAUTHBEARER=20message"),=0A+=09=09=09= =09=09errdetail("The=20server=20does=20not=20support=20channel=20binding=20= for=20OAuth,=20but=20the=20client=20message=20includes=20channel=20= binding=20data."));=0A+=09=09=09break;=0A+=0A+=09=09case=20'y':=09=09=09=09= /*=20fall=20through=20*/=0A+=09=09case=20'n':=0A+=09=09=09p++;=0A+=09=09=09= if=20(*p=20!=3D=20',')=0A+=09=09=09=09ereport(ERROR,=0A+=09=09=09=09=09=09= errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09=09=09=09= errmsg("malformed=20OAUTHBEARER=20message"),=0A+=09=09=09=09=09=09= errdetail("Comma=20expected,=20but=20found=20character=20\"%s\".",=0A+=09= =09=09=09=09=09=09=09=20=20sanitize_char(*p)));=0A+=09=09=09p++;=0A+=09=09= =09break;=0A+=0A+=09=09default:=0A+=09=09=09ereport(ERROR,=0A+=09=09=09=09= =09errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09=09=09= errmsg("malformed=20OAUTHBEARER=20message"),=0A+=09=09=09=09=09= errdetail("Unexpected=20channel-binding=20flag=20\"%s\".",=0A+=09=09=09=09= =09=09=09=20=20sanitize_char(cbind_flag)));=0A+=09}=0A+=0A+=09/*=0A+=09=20= *=20Forbid=20optional=20authzid=20(authorization=20identity).=20=20We=20= don't=20support=20it.=0A+=09=20*/=0A+=09if=20(*p=20=3D=3D=20'a')=0A+=09=09= ereport(ERROR,=0A+=09=09=09=09errcode(ERRCODE_FEATURE_NOT_SUPPORTED),=0A= +=09=09=09=09errmsg("client=20uses=20authorization=20identity,=20but=20= it=20is=20not=20supported"));=0A+=09if=20(*p=20!=3D=20',')=0A+=09=09= ereport(ERROR,=0A+=09=09=09=09errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09= =09=09=09errmsg("malformed=20OAUTHBEARER=20message"),=0A+=09=09=09=09= errdetail("Unexpected=20attribute=20\"%s\"=20in=20= client-first-message.",=0A+=09=09=09=09=09=09=20=20sanitize_char(*p)));=0A= +=09p++;=0A+=0A+=09/*=20All=20remaining=20fields=20are=20separated=20by=20= the=20RFC's=20kvsep=20(\x01).=20*/=0A+=09if=20(*p=20!=3D=20KVSEP)=0A+=09=09= ereport(ERROR,=0A+=09=09=09=09errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09= =09=09=09errmsg("malformed=20OAUTHBEARER=20message"),=0A+=09=09=09=09= errdetail("Key-value=20separator=20expected,=20but=20found=20character=20= \"%s\".",=0A+=09=09=09=09=09=09=20=20sanitize_char(*p)));=0A+=09p++;=0A+=0A= +=09auth=20=3D=20parse_kvpairs_for_auth(&p);=0A+=09if=20(!auth)=0A+=09=09= ereport(ERROR,=0A+=09=09=09=09errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09= =09=09=09errmsg("malformed=20OAUTHBEARER=20message"),=0A+=09=09=09=09= errdetail("Message=20does=20not=20contain=20an=20auth=20value."));=0A+=0A= +=09/*=20We=20should=20be=20at=20the=20end=20of=20our=20message.=20*/=0A= +=09if=20(*p)=0A+=09=09ereport(ERROR,=0A+=09=09=09=09= errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09=09errmsg("malformed=20= OAUTHBEARER=20message"),=0A+=09=09=09=09errdetail("Message=20contains=20= additional=20data=20after=20the=20final=20terminator."));=0A+=0A+=09if=20= (!validate(ctx->port,=20auth))=0A+=09{=0A+=09=09= generate_error_response(ctx,=20output,=20outputlen);=0A+=0A+=09=09= ctx->state=20=3D=20OAUTH_STATE_ERROR;=0A+=09=09status=20=3D=20= PG_SASL_EXCHANGE_CONTINUE;=0A+=09}=0A+=09else=0A+=09{=0A+=09=09= ctx->state=20=3D=20OAUTH_STATE_FINISHED;=0A+=09=09status=20=3D=20= PG_SASL_EXCHANGE_SUCCESS;=0A+=09}=0A+=0A+=09/*=20Don't=20let=20extra=20= copies=20of=20the=20bearer=20token=20hang=20around.=20*/=0A+=09= explicit_bzero(input_copy,=20inputlen);=0A+=0A+=09return=20status;=0A+}=0A= +=0A+/*=0A+=20*=20Convert=20an=20arbitrary=20byte=20to=20printable=20= form.=20=20For=20error=20messages.=0A+=20*=0A+=20*=20If=20it's=20a=20= printable=20ASCII=20character,=20print=20it=20as=20a=20single=20= character.=0A+=20*=20otherwise,=20print=20it=20in=20hex.=0A+=20*=0A+=20*=20= The=20returned=20pointer=20points=20to=20a=20static=20buffer.=0A+=20*/=0A= +static=20char=20*=0A+sanitize_char(char=20c)=0A+{=0A+=09static=20char=20= buf[5];=0A+=0A+=09if=20(c=20>=3D=200x21=20&&=20c=20<=3D=200x7E)=0A+=09=09= snprintf(buf,=20sizeof(buf),=20"'%c'",=20c);=0A+=09else=0A+=09=09= snprintf(buf,=20sizeof(buf),=20"0x%02x",=20(unsigned=20char)=20c);=0A+=09= return=20buf;=0A+}=0A+=0A+/*=0A+=20*=20Performs=20syntactic=20validation=20= of=20a=20key=20and=20value=20from=20the=20initial=20client=0A+=20*=20= response.=20(Semantic=20validation=20of=20interesting=20values=20must=20= be=20performed=0A+=20*=20later.)=0A+=20*/=0A+static=20void=0A= +validate_kvpair(const=20char=20*key,=20const=20char=20*val)=0A+{=0A+=09= /*-----=0A+=09=20*=20=46rom=20Sec=203.1:=0A+=09=20*=20=20=20=20=20key=20=20= =20=20=20=20=20=20=20=20=20=20=3D=201*(ALPHA)=0A+=09=20*/=0A+=09static=20= const=20char=20*key_allowed_set=20=3D=0A+=09=09= "abcdefghijklmnopqrstuvwxyz"=0A+=09=09"ABCDEFGHIJKLMNOPQRSTUVWXYZ";=0A+=0A= +=09size_t=09=09span;=0A+=0A+=09if=20(!key[0])=0A+=09=09ereport(ERROR,=0A= +=09=09=09=09errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09=09= errmsg("malformed=20OAUTHBEARER=20message"),=0A+=09=09=09=09= errdetail("Message=20contains=20an=20empty=20key=20name."));=0A+=0A+=09= span=20=3D=20strspn(key,=20key_allowed_set);=0A+=09if=20(key[span]=20!=3D=20= '\0')=0A+=09=09ereport(ERROR,=0A+=09=09=09=09= errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09=09errmsg("malformed=20= OAUTHBEARER=20message"),=0A+=09=09=09=09errdetail("Message=20contains=20= an=20invalid=20key=20name."));=0A+=0A+=09/*-----=0A+=09=20*=20=46rom=20= Sec=203.1:=0A+=09=20*=20=20=20=20=20value=20=20=20=20=20=20=20=20=20=20=3D= =20*(VCHAR=20/=20SP=20/=20HTAB=20/=20CR=20/=20LF=20)=0A+=09=20*=0A+=09=20= *=20The=20VCHAR=20(visible=20character)=20class=20is=20large;=20a=20loop=20= is=20more=0A+=09=20*=20straightforward=20than=20strspn().=0A+=09=20*/=0A= +=09for=20(;=20*val;=20++val)=0A+=09{=0A+=09=09if=20(0x21=20<=3D=20*val=20= &&=20*val=20<=3D=200x7E)=0A+=09=09=09continue;=09=09=09/*=20VCHAR=20*/=0A= +=0A+=09=09switch=20(*val)=0A+=09=09{=0A+=09=09=09case=20'=20':=0A+=09=09= =09case=20'\t':=0A+=09=09=09case=20'\r':=0A+=09=09=09case=20'\n':=0A+=09=09= =09=09continue;=09=09/*=20SP,=20HTAB,=20CR,=20LF=20*/=0A+=0A+=09=09=09= default:=0A+=09=09=09=09ereport(ERROR,=0A+=09=09=09=09=09=09= errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09=09=09=09= errmsg("malformed=20OAUTHBEARER=20message"),=0A+=09=09=09=09=09=09= errdetail("Message=20contains=20an=20invalid=20value."));=0A+=09=09}=0A+=09= }=0A+}=0A+=0A+/*=0A+=20*=20Consumes=20all=20kvpairs=20in=20an=20= OAUTHBEARER=20exchange=20message.=20If=20the=20"auth"=20key=20is=0A+=20*=20= found,=20its=20value=20is=20returned.=0A+=20*/=0A+static=20char=20*=0A= +parse_kvpairs_for_auth(char=20**input)=0A+{=0A+=09char=09=20=20=20*pos=20= =3D=20*input;=0A+=09char=09=20=20=20*auth=20=3D=20NULL;=0A+=0A+=09/*----=0A= +=09=20*=20The=20relevant=20ABNF,=20from=20Sec.=203.1:=0A+=09=20*=0A+=09=20= *=20=20=20=20=20kvsep=20=20=20=20=20=20=20=20=20=20=3D=20%x01=0A+=09=20*=20= =20=20=20=20key=20=20=20=20=20=20=20=20=20=20=20=20=3D=201*(ALPHA)=0A+=09= =20*=20=20=20=20=20value=20=20=20=20=20=20=20=20=20=20=3D=20*(VCHAR=20/=20= SP=20/=20HTAB=20/=20CR=20/=20LF=20)=0A+=09=20*=20=20=20=20=20kvpair=20=20= =20=20=20=20=20=20=20=3D=20key=20"=3D"=20value=20kvsep=0A+=09=20*=20=20=20= ;;gs2-header=20=20=20=20=20=3D=20See=20RFC=205801=0A+=09=20*=20=20=20=20=20= client-resp=20=20=20=20=3D=20(gs2-header=20kvsep=20*kvpair=20kvsep)=20/=20= kvsep=0A+=09=20*=0A+=09=20*=20By=20the=20time=20we=20reach=20this=20= code,=20the=20gs2-header=20and=20initial=20kvsep=20have=0A+=09=20*=20= already=20been=20validated.=20We=20start=20at=20the=20beginning=20of=20= the=20first=20kvpair.=0A+=09=20*/=0A+=0A+=09while=20(*pos)=0A+=09{=0A+=09= =09char=09=20=20=20*end;=0A+=09=09char=09=20=20=20*sep;=0A+=09=09char=09=20= =20=20*key;=0A+=09=09char=09=20=20=20*value;=0A+=0A+=09=09/*=0A+=09=09=20= *=20Find=20the=20end=20of=20this=20kvpair.=20Note=20that=20input=20is=20= null-terminated=20by=0A+=09=09=20*=20the=20SASL=20code,=20so=20the=20= strchr()=20is=20bounded.=0A+=09=09=20*/=0A+=09=09end=20=3D=20strchr(pos,=20= KVSEP);=0A+=09=09if=20(!end)=0A+=09=09=09ereport(ERROR,=0A+=09=09=09=09=09= errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09=09=09errmsg("malformed=20= OAUTHBEARER=20message"),=0A+=09=09=09=09=09errdetail("Message=20contains=20= an=20unterminated=20key/value=20pair."));=0A+=09=09*end=20=3D=20'\0';=0A= +=0A+=09=09if=20(pos=20=3D=3D=20end)=0A+=09=09{=0A+=09=09=09/*=20Empty=20= kvpair,=20signifying=20the=20end=20of=20the=20list.=20*/=0A+=09=09=09= *input=20=3D=20pos=20+=201;=0A+=09=09=09return=20auth;=0A+=09=09}=0A+=0A= +=09=09/*=0A+=09=09=20*=20Find=20the=20end=20of=20the=20key=20name.=0A+=09= =09=20*/=0A+=09=09sep=20=3D=20strchr(pos,=20'=3D');=0A+=09=09if=20(!sep)=0A= +=09=09=09ereport(ERROR,=0A+=09=09=09=09=09= errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09=09=09errmsg("malformed=20= OAUTHBEARER=20message"),=0A+=09=09=09=09=09errdetail("Message=20contains=20= a=20key=20without=20a=20value."));=0A+=09=09*sep=20=3D=20'\0';=0A+=0A+=09= =09/*=20Both=20key=20and=20value=20are=20now=20safely=20terminated.=20*/=0A= +=09=09key=20=3D=20pos;=0A+=09=09value=20=3D=20sep=20+=201;=0A+=09=09= validate_kvpair(key,=20value);=0A+=0A+=09=09if=20(strcmp(key,=20= AUTH_KEY)=20=3D=3D=200)=0A+=09=09{=0A+=09=09=09if=20(auth)=0A+=09=09=09=09= ereport(ERROR,=0A+=09=09=09=09=09=09errcode(ERRCODE_PROTOCOL_VIOLATION),=0A= +=09=09=09=09=09=09errmsg("malformed=20OAUTHBEARER=20message"),=0A+=09=09= =09=09=09=09errdetail("Message=20contains=20multiple=20auth=20= values."));=0A+=0A+=09=09=09auth=20=3D=20value;=0A+=09=09}=0A+=09=09else=0A= +=09=09{=0A+=09=09=09/*=0A+=09=09=09=20*=20The=20RFC=20also=20defines=20= the=20host=20and=20port=20keys,=20but=20they=20are=20not=0A+=09=09=09=20= *=20required=20for=20OAUTHBEARER=20and=20we=20do=20not=20use=20them.=20= Also,=20per=20Sec.=0A+=09=09=09=20*=203.1,=20any=20key/value=20pairs=20= we=20don't=20recognize=20must=20be=20ignored.=0A+=09=09=09=20*/=0A+=09=09= }=0A+=0A+=09=09/*=20Move=20to=20the=20next=20pair.=20*/=0A+=09=09pos=20=3D= =20end=20+=201;=0A+=09}=0A+=0A+=09ereport(ERROR,=0A+=09=09=09= errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09errmsg("malformed=20= OAUTHBEARER=20message"),=0A+=09=09=09errdetail("Message=20did=20not=20= contain=20a=20final=20terminator."));=0A+=0A+=09pg_unreachable();=0A+=09= return=20NULL;=0A+}=0A+=0A+/*=0A+=20*=20Builds=20the=20JSON=20response=20= for=20failed=20authentication=20(RFC=207628,=20Sec.=203.2.2).=0A+=20*=20= This=20contains=20the=20required=20scopes=20for=20entry=20and=20a=20= pointer=20to=20the=20OAuth/OpenID=0A+=20*=20discovery=20document,=20= which=20the=20client=20may=20use=20to=20conduct=20its=20OAuth=20flow.=0A= +=20*/=0A+static=20void=0A+generate_error_response(struct=20oauth_ctx=20= *ctx,=20char=20**output,=20int=20*outputlen)=0A+{=0A+=09StringInfoData=20= buf;=0A+=09StringInfoData=20issuer;=0A+=0A+=09/*=0A+=09=20*=20The=20= admin=20needs=20to=20set=20an=20issuer=20and=20scope=20for=20OAuth=20to=20= work.=20There's=0A+=09=20*=20not=20really=20a=20way=20to=20hide=20this=20= from=20the=20user,=20either,=20because=20we=20can't=0A+=09=20*=20choose=20= a=20"default"=20issuer,=20so=20be=20honest=20in=20the=20failure=20= message.=20(In=0A+=09=20*=20practice=20such=20configurations=20are=20= rejected=20during=20HBA=20parsing.)=0A+=09=20*/=0A+=09if=20(!ctx->issuer=20= ||=20!ctx->scope)=0A+=09=09ereport(FATAL,=0A+=09=09=09=09= errcode(ERRCODE_INTERNAL_ERROR),=0A+=09=09=09=09errmsg("OAuth=20is=20not=20= properly=20configured=20for=20this=20user"),=0A+=09=09=09=09= errdetail_log("The=20issuer=20and=20scope=20parameters=20must=20be=20set=20= in=20pg_hba.conf."));=0A+=0A+=09/*=0A+=09=20*=20Build=20a=20default=20= .well-known=20URI=20based=20on=20our=20issuer,=20unless=20the=20HBA=20= has=0A+=09=20*=20already=20provided=20one.=0A+=09=20*/=0A+=09= initStringInfo(&issuer);=0A+=09appendStringInfoString(&issuer,=20= ctx->issuer);=0A+=09if=20(strstr(ctx->issuer,=20"/.well-known/")=20=3D=3D=20= NULL)=0A+=09=09appendStringInfoString(&issuer,=20= "/.well-known/openid-configuration");=0A+=0A+=09initStringInfo(&buf);=0A= +=0A+=09/*=0A+=09=20*=20Escaping=20the=20string=20here=20is=20= belt-and-suspenders=20defensive=20programming=0A+=09=20*=20since=20= escapable=20characters=20aren't=20valid=20in=20either=20the=20issuer=20= URI=20or=20the=0A+=09=20*=20scope=20list,=20but=20the=20HBA=20doesn't=20= enforce=20that=20yet.=0A+=09=20*/=0A+=09appendStringInfoString(&buf,=20= "{=20\"status\":=20\"invalid_token\",=20");=0A+=0A+=09= appendStringInfoString(&buf,=20"\"openid-configuration\":=20");=0A+=09= escape_json(&buf,=20issuer.data);=0A+=09pfree(issuer.data);=0A+=0A+=09= appendStringInfoString(&buf,=20",=20\"scope\":=20");=0A+=09= escape_json(&buf,=20ctx->scope);=0A+=0A+=09appendStringInfoString(&buf,=20= "=20}");=0A+=0A+=09*output=20=3D=20buf.data;=0A+=09*outputlen=20=3D=20= buf.len;=0A+}=0A+=0A+/*-----=0A+=20*=20Validates=20the=20provided=20= Authorization=20header=20and=20returns=20the=20token=20from=0A+=20*=20= within=20it.=20NULL=20is=20returned=20on=20validation=20failure.=0A+=20*=0A= +=20*=20Only=20Bearer=20tokens=20are=20accepted.=20The=20ABNF=20is=20= defined=20in=20RFC=206750,=20Sec.=0A+=20*=202.1:=0A+=20*=0A+=20*=20=20=20= =20=20=20b64token=20=20=20=20=3D=201*(=20ALPHA=20/=20DIGIT=20/=0A+=20*=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20"-"=20= /=20"."=20/=20"_"=20/=20"~"=20/=20"+"=20/=20"/"=20)=20*"=3D"=0A+=20*=20=20= =20=20=20=20credentials=20=3D=20"Bearer"=201*SP=20b64token=0A+=20*=0A+=20= *=20The=20"credentials"=20construction=20is=20what=20we=20receive=20in=20= our=20auth=20value.=0A+=20*=0A+=20*=20Since=20that=20spec=20is=20= subordinate=20to=20HTTP=20(i.e.=20the=20HTTP=20Authorization=0A+=20*=20= header=20format;=20RFC=209110=20Sec.=2011),=20the=20"Bearer"=20scheme=20= string=20must=20be=0A+=20*=20compared=20case-insensitively.=20(This=20is=20= not=20mentioned=20in=20RFC=206750,=20but=20the=0A+=20*=20OAUTHBEARER=20= spec=20points=20it=20out:=20RFC=207628=20Sec.=204.)=0A+=20*=0A+=20*=20= Invalid=20formats=20are=20technically=20a=20protocol=20violation,=20but=20= we=20shouldn't=0A+=20*=20reflect=20any=20information=20about=20the=20= sensitive=20Bearer=20token=20back=20to=20the=0A+=20*=20client;=20log=20= at=20COMMERROR=20instead.=0A+=20*/=0A+static=20const=20char=20*=0A= +validate_token_format(const=20char=20*header)=0A+{=0A+=09size_t=09=09= span;=0A+=09const=20char=20*token;=0A+=09static=20const=20char=20*const=20= b64token_allowed_set=20=3D=0A+=09=09"abcdefghijklmnopqrstuvwxyz"=0A+=09=09= "ABCDEFGHIJKLMNOPQRSTUVWXYZ"=0A+=09=09"0123456789-._~+/";=0A+=0A+=09/*=20= Missing=20auth=20headers=20should=20be=20handled=20by=20the=20caller.=20= */=0A+=09Assert(header);=0A+=0A+=09if=20(header[0]=20=3D=3D=20'\0')=0A+=09= {=0A+=09=09/*=0A+=09=09=20*=20A=20completely=20empty=20auth=20header=20= represents=20a=20query=20for=0A+=09=09=20*=20authentication=20= parameters.=20The=20client=20expects=20it=20to=20fail;=20there's=0A+=09=09= =20*=20no=20need=20to=20make=20any=20extra=20noise=20in=20the=20logs.=0A= +=09=09=20*=0A+=09=09=20*=20TODO:=20should=20we=20find=20a=20way=20to=20= return=20STATUS_EOF=20at=20the=20top=20level,=0A+=09=09=20*=20to=20= suppress=20the=20authentication=20error=20entirely?=0A+=09=09=20*/=0A+=09= =09return=20NULL;=0A+=09}=0A+=0A+=09if=20(pg_strncasecmp(header,=20= BEARER_SCHEME,=20strlen(BEARER_SCHEME)))=0A+=09{=0A+=09=09= ereport(COMMERROR,=0A+=09=09=09=09errcode(ERRCODE_PROTOCOL_VIOLATION),=0A= +=09=09=09=09errmsg("malformed=20OAuth=20bearer=20token"),=0A+=09=09=09=09= errdetail_log("Client=20response=20indicated=20a=20non-Bearer=20= authentication=20scheme."));=0A+=09=09return=20NULL;=0A+=09}=0A+=0A+=09= /*=20Pull=20the=20bearer=20token=20out=20of=20the=20auth=20value.=20*/=0A= +=09token=20=3D=20header=20+=20strlen(BEARER_SCHEME);=0A+=0A+=09/*=20= Swallow=20any=20additional=20spaces.=20*/=0A+=09while=20(*token=20=3D=3D=20= '=20')=0A+=09=09token++;=0A+=0A+=09/*=20Tokens=20must=20not=20be=20= empty.=20*/=0A+=09if=20(!*token)=0A+=09{=0A+=09=09ereport(COMMERROR,=0A+=09= =09=09=09errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09=09= errmsg("malformed=20OAuth=20bearer=20token"),=0A+=09=09=09=09= errdetail_log("Bearer=20token=20is=20empty."));=0A+=09=09return=20NULL;=0A= +=09}=0A+=0A+=09/*=0A+=09=20*=20Make=20sure=20the=20token=20contains=20= only=20allowed=20characters.=20Tokens=20may=20end=0A+=09=20*=20with=20= any=20number=20of=20'=3D'=20characters.=0A+=09=20*/=0A+=09span=20=3D=20= strspn(token,=20b64token_allowed_set);=0A+=09while=20(token[span]=20=3D=3D= =20'=3D')=0A+=09=09span++;=0A+=0A+=09if=20(token[span]=20!=3D=20'\0')=0A= +=09{=0A+=09=09/*=0A+=09=09=20*=20This=20error=20message=20could=20be=20= more=20helpful=20by=20printing=20the=0A+=09=09=20*=20problematic=20= character(s),=20but=20that'd=20be=20a=20bit=20like=20printing=20a=20= piece=0A+=09=09=20*=20of=20someone's=20password=20into=20the=20logs.=0A+=09= =09=20*/=0A+=09=09ereport(COMMERROR,=0A+=09=09=09=09= errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09=09errmsg("malformed=20= OAuth=20bearer=20token"),=0A+=09=09=09=09errdetail_log("Bearer=20token=20= is=20not=20in=20the=20correct=20format."));=0A+=09=09return=20NULL;=0A+=09= }=0A+=0A+=09return=20token;=0A+}=0A+=0A+/*=0A+=20*=20Checks=20that=20the=20= "auth"=20kvpair=20in=20the=20client=20response=20contains=20a=20= syntactically=0A+=20*=20valid=20Bearer=20token,=20then=20passes=20it=20= along=20to=20the=20loaded=20validator=20module=20for=0A+=20*=20= authorization.=20Returns=20true=20if=20validation=20succeeds.=0A+=20*/=0A= +static=20bool=0A+validate(Port=20*port,=20const=20char=20*auth)=0A+{=0A= +=09int=09=09=09map_status;=0A+=09ValidatorModuleResult=20*ret;=0A+=09= const=20char=20*token;=0A+=09bool=09=09status;=0A+=0A+=09/*=20Ensure=20= that=20we=20have=20a=20correct=20token=20to=20validate=20*/=0A+=09if=20= (!(token=20=3D=20validate_token_format(auth)))=0A+=09=09return=20false;=0A= +=0A+=09/*=0A+=09=20*=20Ensure=20that=20we=20have=20a=20validation=20= library=20loaded,=20this=20should=20always=20be=0A+=09=20*=20the=20case=20= and=20an=20error=20here=20is=20indicative=20of=20a=20bug.=0A+=09=20*/=0A= +=09if=20(!ValidatorCallbacks=20||=20!ValidatorCallbacks->validate_cb)=0A= +=09=09ereport(FATAL,=0A+=09=09=09=09errcode(ERRCODE_INTERNAL_ERROR),=0A= +=09=09=09=09errmsg("validation=20of=20OAuth=20token=20requested=20= without=20a=20validator=20loaded"));=0A+=0A+=09/*=20Call=20the=20= validation=20function=20from=20the=20validator=20module=20*/=0A+=09ret=20= =3D=20ValidatorCallbacks->validate_cb(validator_module_state,=0A+=09=09=09= =09=09=09=09=09=09=09=20=20token,=20port->user_name);=0A+=09if=20(ret=20= =3D=3D=20NULL)=0A+=09{=0A+=09=09ereport(LOG,=20errmsg("internal=20error=20= in=20OAuth=20validator=20module"));=0A+=09=09return=20false;=0A+=09}=0A+=0A= +=09/*=0A+=09=20*=20Log=20any=20authentication=20results=20even=20if=20= the=20token=20isn't=20authorized;=20it=0A+=09=20*=20might=20be=20useful=20= for=20auditing=20or=20troubleshooting.=0A+=09=20*/=0A+=09if=20= (ret->authn_id)=0A+=09=09set_authn_id(port,=20ret->authn_id);=0A+=0A+=09= if=20(!ret->authorized)=0A+=09{=0A+=09=09ereport(LOG,=0A+=09=09=09=09= errmsg("OAuth=20bearer=20authentication=20failed=20for=20user=20\"%s\"",=0A= +=09=09=09=09=09=20=20=20port->user_name),=0A+=09=09=09=09= errdetail_log("Validator=20failed=20to=20authorize=20the=20provided=20= token."));=0A+=0A+=09=09status=20=3D=20false;=0A+=09=09goto=20cleanup;=0A= +=09}=0A+=0A+=09if=20(port->hba->oauth_skip_usermap)=0A+=09{=0A+=09=09/*=0A= +=09=09=20*=20If=20the=20validator=20is=20our=20authorization=20= authority,=20we're=20done.=0A+=09=09=20*=20Authentication=20may=20or=20= may=20not=20have=20been=20performed=20depending=20on=20the=0A+=09=09=20*=20= validator=20implementation;=20all=20that=20matters=20is=20that=20the=20= validator=0A+=09=09=20*=20says=20the=20user=20can=20log=20in=20with=20= the=20target=20role.=0A+=09=09=20*/=0A+=09=09status=20=3D=20true;=0A+=09=09= goto=20cleanup;=0A+=09}=0A+=0A+=09/*=20Make=20sure=20the=20validator=20= authenticated=20the=20user.=20*/=0A+=09if=20(ret->authn_id=20=3D=3D=20= NULL=20||=20ret->authn_id[0]=20=3D=3D=20'\0')=0A+=09{=0A+=09=09= ereport(LOG,=0A+=09=09=09=09errmsg("OAuth=20bearer=20authentication=20= failed=20for=20user=20\"%s\"",=0A+=09=09=09=09=09=20=20=20= port->user_name),=0A+=09=09=09=09errdetail_log("Validator=20provided=20= no=20identity."));=0A+=0A+=09=09status=20=3D=20false;=0A+=09=09goto=20= cleanup;=0A+=09}=0A+=0A+=09/*=20Finally,=20check=20the=20user=20map.=20= */=0A+=09map_status=20=3D=20check_usermap(port->hba->usermap,=20= port->user_name,=0A+=09=09=09=09=09=09=09=20=20=20= MyClientConnectionInfo.authn_id,=20false);=0A+=09status=20=3D=20= (map_status=20=3D=3D=20STATUS_OK);=0A+=0A+cleanup:=0A+=0A+=09/*=0A+=09=20= *=20Clear=20and=20free=20the=20validation=20result=20from=20the=20= validator=20module=20once=0A+=09=20*=20we're=20done=20with=20it.=0A+=09=20= */=0A+=09if=20(ret->authn_id=20!=3D=20NULL)=0A+=09=09= pfree(ret->authn_id);=0A+=09pfree(ret);=0A+=0A+=09return=20status;=0A+}=0A= +=0A+/*=0A+=20*=20load_validator_library=0A+=20*=0A+=20*=20Load=20the=20= configured=20validator=20library=20in=20order=20to=20perform=20token=20= validation.=0A+=20*=20There=20is=20no=20built-in=20fallback=20since=20= validation=20is=20implementation=20specific.=20If=0A+=20*=20no=20= validator=20library=20is=20configured,=20or=20if=20it=20fails=20to=20= load,=20then=20error=20out=0A+=20*=20since=20token=20validation=20won't=20= be=20possible.=0A+=20*/=0A+static=20void=0A+load_validator_library(const=20= char=20*libname)=0A+{=0A+=09OAuthValidatorModuleInit=20validator_init;=0A= +=09MemoryContextCallback=20*mcb;=0A+=0A+=09Assert(libname=20&&=20= *libname);=0A+=0A+=09validator_init=20=3D=20(OAuthValidatorModuleInit)=0A= +=09=09load_external_function(libname,=20= "_PG_oauth_validator_module_init",=0A+=09=09=09=09=09=09=09=20=20=20= false,=20NULL);=0A+=0A+=09/*=0A+=09=20*=20The=20validator=20init=20= function=20is=20required=20since=20it=20will=20set=20the=20callbacks=0A+=09= =20*=20for=20the=20validator=20library.=0A+=09=20*/=0A+=09if=20= (validator_init=20=3D=3D=20NULL)=0A+=09=09ereport(ERROR,=0A+=09=09=09=09= errmsg("%s=20module=20\"%s\"=20must=20define=20the=20symbol=20%s",=0A+=09= =09=09=09=09=20=20=20"OAuth=20validator",=20libname,=20= "_PG_oauth_validator_module_init"));=0A+=0A+=09ValidatorCallbacks=20=3D=20= (*validator_init)=20();=0A+=09Assert(ValidatorCallbacks);=0A+=0A+=09/*=20= Allocate=20memory=20for=20validator=20library=20private=20state=20data=20= */=0A+=09validator_module_state=20=3D=20(ValidatorModuleState=20*)=20= palloc0(sizeof(ValidatorModuleState));=0A+=09if=20= (ValidatorCallbacks->startup_cb=20!=3D=20NULL)=0A+=09=09= ValidatorCallbacks->startup_cb(validator_module_state);=0A+=0A+=09/*=20= Shut=20down=20the=20library=20before=20cleaning=20up=20its=20state.=20*/=0A= +=09mcb=20=3D=20palloc0(sizeof(*mcb));=0A+=09mcb->func=20=3D=20= shutdown_validator_library;=0A+=0A+=09= MemoryContextRegisterResetCallback(CurrentMemoryContext,=20mcb);=0A+}=0A= +=0A+/*=0A+=20*=20Call=20the=20validator=20module's=20shutdown=20= callback,=20if=20one=20is=20provided.=20This=20is=0A+=20*=20invoked=20= during=20memory=20context=20reset.=0A+=20*/=0A+static=20void=0A= +shutdown_validator_library(void=20*arg)=0A+{=0A+=09if=20= (ValidatorCallbacks->shutdown_cb=20!=3D=20NULL)=0A+=09=09= ValidatorCallbacks->shutdown_cb(validator_module_state);=0A+}=0A+=0A+/*=0A= +=20*=20Ensure=20an=20OAuth=20validator=20named=20in=20the=20HBA=20is=20= permitted=20by=20the=20configuration.=0A+=20*=0A+=20*=20If=20the=20= validator=20is=20currently=20unset=20and=20exactly=20one=20library=20is=20= declared=20in=0A+=20*=20oauth_validator_libraries,=20then=20that=20= library=20will=20be=20used=20as=20the=20validator.=0A+=20*=20Otherwise=20= the=20name=20must=20be=20present=20in=20the=20list=20of=20= oauth_validator_libraries.=0A+=20*/=0A+bool=0A= +check_oauth_validator(HbaLine=20*hbaline,=20int=20elevel,=20char=20= **err_msg)=0A+{=0A+=09int=09=09=09line_num=20=3D=20hbaline->linenumber;=0A= +=09char=09=20=20=20*file_name=20=3D=20hbaline->sourcefile;=0A+=09char=09= =20=20=20*rawstring;=0A+=09List=09=20=20=20*elemlist=20=3D=20NIL;=0A+=0A= +=09*err_msg=20=3D=20NULL;=0A+=0A+=09if=20= (oauth_validator_libraries_string[0]=20=3D=3D=20'\0')=0A+=09{=0A+=09=09= ereport(elevel,=0A+=09=09=09=09errcode(ERRCODE_CONFIG_FILE_ERROR),=0A+=09= =09=09=09errmsg("oauth_validator_libraries=20must=20be=20set=20for=20= authentication=20method=20%s",=0A+=09=09=09=09=09=20=20=20"oauth"),=0A+=09= =09=09=09errcontext("line=20%d=20of=20configuration=20file=20\"%s\"",=0A= +=09=09=09=09=09=09=20=20=20line_num,=20file_name));=0A+=09=09*err_msg=20= =3D=20psprintf("oauth_validator_libraries=20must=20be=20set=20for=20= authentication=20method=20%s",=0A+=09=09=09=09=09=09=09"oauth");=0A+=09=09= return=20false;=0A+=09}=0A+=0A+=09/*=20SplitDirectoriesString=20needs=20= a=20modifiable=20copy=20*/=0A+=09rawstring=20=3D=20= pstrdup(oauth_validator_libraries_string);=0A+=0A+=09if=20= (!SplitDirectoriesString(rawstring,=20',',=20&elemlist))=0A+=09{=0A+=09=09= /*=20syntax=20error=20in=20list=20*/=0A+=09=09ereport(elevel,=0A+=09=09=09= =09errcode(ERRCODE_CONFIG_FILE_ERROR),=0A+=09=09=09=09errmsg("invalid=20= list=20syntax=20in=20parameter=20\"%s\"",=0A+=09=09=09=09=09=20=20=20= "oauth_validator_libraries"));=0A+=09=09*err_msg=20=3D=20= psprintf("invalid=20list=20syntax=20in=20parameter=20\"%s\"",=0A+=09=09=09= =09=09=09=09"oauth_validator_libraries");=0A+=09=09goto=20done;=0A+=09}=0A= +=0A+=09if=20(!hbaline->oauth_validator)=0A+=09{=0A+=09=09if=20= (elemlist->length=20=3D=3D=201)=0A+=09=09{=0A+=09=09=09= hbaline->oauth_validator=20=3D=20pstrdup(linitial(elemlist));=0A+=09=09=09= goto=20done;=0A+=09=09}=0A+=0A+=09=09ereport(elevel,=0A+=09=09=09=09= errcode(ERRCODE_CONFIG_FILE_ERROR),=0A+=09=09=09=09= errmsg("authentication=20method=20\"oauth\"=20requires=20argument=20= \"validator\"=20to=20be=20set=20when=20oauth_validator_libraries=20= contains=20multiple=20options"),=0A+=09=09=09=09errcontext("line=20%d=20= of=20configuration=20file=20\"%s\"",=0A+=09=09=09=09=09=09=20=20=20= line_num,=20file_name));=0A+=09=09*err_msg=20=3D=20"authentication=20= method=20\"oauth\"=20requires=20argument=20\"validator\"=20to=20be=20set=20= when=20oauth_validator_libraries=20contains=20multiple=20options";=0A+=09= =09goto=20done;=0A+=09}=0A+=0A+=09foreach_ptr(char,=20allowed,=20= elemlist)=0A+=09{=0A+=09=09if=20(strcmp(allowed,=20= hbaline->oauth_validator)=20=3D=3D=200)=0A+=09=09=09goto=20done;=0A+=09}=0A= +=0A+=09ereport(elevel,=0A+=09=09=09= errcode(ERRCODE_INVALID_PARAMETER_VALUE),=0A+=09=09=09errmsg("validator=20= \"%s\"=20is=20not=20permitted=20by=20%s",=0A+=09=09=09=09=20=20=20= hbaline->oauth_validator,=20"oauth_validator_libraries"),=0A+=09=09=09= errcontext("line=20%d=20of=20configuration=20file=20\"%s\"",=0A+=09=09=09= =09=09=20=20=20line_num,=20file_name));=0A+=09*err_msg=20=3D=20= psprintf("validator=20\"%s\"=20is=20not=20permitted=20by=20%s",=0A+=09=09= =09=09=09=09hbaline->oauth_validator,=20"oauth_validator_libraries");=0A= +=0A+done:=0A+=09list_free_deep(elemlist);=0A+=09pfree(rawstring);=0A+=0A= +=09return=20(*err_msg=20=3D=3D=20NULL);=0A+}=0Adiff=20--git=20= a/src/backend/libpq/auth.c=20b/src/backend/libpq/auth.c=0Aindex=20= d6ef32cc823..0f65014e64f=20100644=0A---=20a/src/backend/libpq/auth.c=0A= +++=20b/src/backend/libpq/auth.c=0A@@=20-29,6=20+29,7=20@@=0A=20#include=20= "libpq/auth.h"=0A=20#include=20"libpq/crypt.h"=0A=20#include=20= "libpq/libpq.h"=0A+#include=20"libpq/oauth.h"=0A=20#include=20= "libpq/pqformat.h"=0A=20#include=20"libpq/sasl.h"=0A=20#include=20= "libpq/scram.h"=0A@@=20-45,7=20+46,6=20@@=0A=20=20*/=0A=20static=20void=20= auth_failed(Port=20*port,=20int=20status,=20const=20char=20*logdetail);=0A= =20static=20char=20*recv_password_packet(Port=20*port);=0A-static=20void=20= set_authn_id(Port=20*port,=20const=20char=20*id);=0A=20=0A=20=0A=20= /*----------------------------------------------------------------=0A@@=20= -289,6=20+289,9=20@@=20auth_failed(Port=20*port,=20int=20status,=20const=20= char=20*logdetail)=0A=20=09=09case=20uaRADIUS:=0A=20=09=09=09errstr=20=3D=20= gettext_noop("RADIUS=20authentication=20failed=20for=20user=20\"%s\"");=0A= =20=09=09=09break;=0A+=09=09case=20uaOAuth:=0A+=09=09=09errstr=20=3D=20= gettext_noop("OAuth=20bearer=20authentication=20failed=20for=20user=20= \"%s\"");=0A+=09=09=09break;=0A=20=09=09default:=0A=20=09=09=09errstr=20= =3D=20gettext_noop("authentication=20failed=20for=20user=20\"%s\":=20= invalid=20authentication=20method");=0A=20=09=09=09break;=0A@@=20-324,7=20= +327,7=20@@=20auth_failed(Port=20*port,=20int=20status,=20const=20char=20= *logdetail)=0A=20=20*=20lifetime=20of=20MyClientConnectionInfo,=20so=20= it=20is=20safe=20to=20pass=20a=20string=20that=20is=0A=20=20*=20managed=20= by=20an=20external=20library.=0A=20=20*/=0A-static=20void=0A+void=0A=20= set_authn_id(Port=20*port,=20const=20char=20*id)=0A=20{=0A=20=09= Assert(id);=0A@@=20-611,6=20+614,9=20@@=20ClientAuthentication(Port=20= *port)=0A=20=09=09case=20uaTrust:=0A=20=09=09=09status=20=3D=20= STATUS_OK;=0A=20=09=09=09break;=0A+=09=09case=20uaOAuth:=0A+=09=09=09= status=20=3D=20CheckSASLAuth(&pg_be_oauth_mech,=20port,=20NULL,=20NULL);=0A= +=09=09=09break;=0A=20=09}=0A=20=0A=20=09if=20((status=20=3D=3D=20= STATUS_OK=20&&=20port->hba->clientcert=20=3D=3D=20clientCertFull)=0Adiff=20= --git=20a/src/backend/libpq/hba.c=20b/src/backend/libpq/hba.c=0Aindex=20= 510c9ffc6d7..332fad27835=20100644=0A---=20a/src/backend/libpq/hba.c=0A= +++=20b/src/backend/libpq/hba.c=0A@@=20-32,6=20+32,7=20@@=0A=20#include=20= "libpq/hba.h"=0A=20#include=20"libpq/ifaddr.h"=0A=20#include=20= "libpq/libpq-be.h"=0A+#include=20"libpq/oauth.h"=0A=20#include=20= "postmaster/postmaster.h"=0A=20#include=20"regex/regex.h"=0A=20#include=20= "replication/walsender.h"=0A@@=20-114,7=20+115,8=20@@=20static=20const=20= char=20*const=20UserAuthName[]=20=3D=0A=20=09"ldap",=0A=20=09"cert",=0A=20= =09"radius",=0A-=09"peer"=0A+=09"peer",=0A+=09"oauth",=0A=20};=0A=20=0A=20= /*=0A@@=20-1747,6=20+1749,8=20@@=20parse_hba_line(TokenizedAuthLine=20= *tok_line,=20int=20elevel)=0A=20#endif=0A=20=09else=20if=20= (strcmp(token->string,=20"radius")=20=3D=3D=200)=0A=20=09=09= parsedline->auth_method=20=3D=20uaRADIUS;=0A+=09else=20if=20= (strcmp(token->string,=20"oauth")=20=3D=3D=200)=0A+=09=09= parsedline->auth_method=20=3D=20uaOAuth;=0A=20=09else=0A=20=09{=0A=20=09=09= ereport(elevel,=0A@@=20-2039,6=20+2043,36=20@@=20= parse_hba_line(TokenizedAuthLine=20*tok_line,=20int=20elevel)=0A=20=09=09= parsedline->clientcert=20=3D=20clientCertFull;=0A=20=09}=0A=20=0A+=09/*=0A= +=09=20*=20Enforce=20proper=20configuration=20of=20OAuth=20= authentication.=0A+=09=20*/=0A+=09if=20(parsedline->auth_method=20=3D=3D=20= uaOAuth)=0A+=09{=0A+=09=09MANDATORY_AUTH_ARG(parsedline->oauth_scope,=20= "scope",=20"oauth");=0A+=09=09= MANDATORY_AUTH_ARG(parsedline->oauth_issuer,=20"issuer",=20"oauth");=0A+=0A= +=09=09/*=20Ensure=20a=20validator=20library=20is=20set=20and=20= permitted=20by=20the=20config.=20*/=0A+=09=09if=20= (!check_oauth_validator(parsedline,=20elevel,=20err_msg))=0A+=09=09=09= return=20NULL;=0A+=0A+=09=09/*=0A+=09=09=20*=20Supplying=20a=20usermap=20= combined=20with=20the=20option=20to=20skip=20usermapping=20is=0A+=09=09=20= *=20nonsensical=20and=20indicates=20a=20configuration=20error.=0A+=09=09=20= */=0A+=09=09if=20(parsedline->oauth_skip_usermap=20&&=20= parsedline->usermap=20!=3D=20NULL)=0A+=09=09{=0A+=09=09=09= ereport(elevel,=0A+=09=09=09=09=09errcode(ERRCODE_CONFIG_FILE_ERROR),=0A= +=09=09=09/*=20translator:=20strings=20are=20replaced=20with=20hba=20= options=20*/=0A+=09=09=09=09=09errmsg("%s=20cannot=20be=20used=20in=20= combination=20with=20%s",=0A+=09=09=09=09=09=09=20=20=20"map",=20= "delegate_ident_mapping"),=0A+=09=09=09=09=09errcontext("line=20%d=20of=20= configuration=20file=20\"%s\"",=0A+=09=09=09=09=09=09=09=20=20=20= line_num,=20file_name));=0A+=09=09=09*err_msg=20=3D=20"map=20cannot=20be=20= used=20in=20combination=20with=20delegate_ident_mapping";=0A+=09=09=09= return=20NULL;=0A+=09=09}=0A+=09}=0A+=0A=20=09return=20parsedline;=0A=20= }=0A=20=0A@@=20-2066,8=20+2100,9=20@@=20parse_hba_auth_opt(char=20*name,=20= char=20*val,=20HbaLine=20*hbaline,=0A=20=09=09=09hbaline->auth_method=20= !=3D=20uaPeer=20&&=0A=20=09=09=09hbaline->auth_method=20!=3D=20uaGSS=20= &&=0A=20=09=09=09hbaline->auth_method=20!=3D=20uaSSPI=20&&=0A-=09=09=09= hbaline->auth_method=20!=3D=20uaCert)=0A-=09=09=09= INVALID_AUTH_OPTION("map",=20gettext_noop("ident,=20peer,=20gssapi,=20= sspi,=20and=20cert"));=0A+=09=09=09hbaline->auth_method=20!=3D=20uaCert=20= &&=0A+=09=09=09hbaline->auth_method=20!=3D=20uaOAuth)=0A+=09=09=09= INVALID_AUTH_OPTION("map",=20gettext_noop("ident,=20peer,=20gssapi,=20= sspi,=20cert,=20and=20oauth"));=0A=20=09=09hbaline->usermap=20=3D=20= pstrdup(val);=0A=20=09}=0A=20=09else=20if=20(strcmp(name,=20= "clientcert")=20=3D=3D=200)=0A@@=20-2450,6=20+2485,29=20@@=20= parse_hba_auth_opt(char=20*name,=20char=20*val,=20HbaLine=20*hbaline,=0A=20= =09=09hbaline->radiusidentifiers=20=3D=20parsed_identifiers;=0A=20=09=09= hbaline->radiusidentifiers_s=20=3D=20pstrdup(val);=0A=20=09}=0A+=09else=20= if=20(strcmp(name,=20"issuer")=20=3D=3D=200)=0A+=09{=0A+=09=09= REQUIRE_AUTH_OPTION(uaOAuth,=20"issuer",=20"oauth");=0A+=09=09= hbaline->oauth_issuer=20=3D=20pstrdup(val);=0A+=09}=0A+=09else=20if=20= (strcmp(name,=20"scope")=20=3D=3D=200)=0A+=09{=0A+=09=09= REQUIRE_AUTH_OPTION(uaOAuth,=20"scope",=20"oauth");=0A+=09=09= hbaline->oauth_scope=20=3D=20pstrdup(val);=0A+=09}=0A+=09else=20if=20= (strcmp(name,=20"validator")=20=3D=3D=200)=0A+=09{=0A+=09=09= REQUIRE_AUTH_OPTION(uaOAuth,=20"validator",=20"oauth");=0A+=09=09= hbaline->oauth_validator=20=3D=20pstrdup(val);=0A+=09}=0A+=09else=20if=20= (strcmp(name,=20"delegate_ident_mapping")=20=3D=3D=200)=0A+=09{=0A+=09=09= REQUIRE_AUTH_OPTION(uaOAuth,=20"delegate_ident_mapping",=20"oauth");=0A+=09= =09if=20(strcmp(val,=20"1")=20=3D=3D=200)=0A+=09=09=09= hbaline->oauth_skip_usermap=20=3D=20true;=0A+=09=09else=0A+=09=09=09= hbaline->oauth_skip_usermap=20=3D=20false;=0A+=09}=0A=20=09else=0A=20=09= {=0A=20=09=09ereport(elevel,=0Adiff=20--git=20= a/src/backend/libpq/meson.build=20b/src/backend/libpq/meson.build=0A= index=200f0421037e4..31aa2faae1e=20100644=0A---=20= a/src/backend/libpq/meson.build=0A+++=20b/src/backend/libpq/meson.build=0A= @@=20-1,6=20+1,7=20@@=0A=20#=20Copyright=20(c)=202022-2025,=20PostgreSQL=20= Global=20Development=20Group=0A=20=0A=20backend_sources=20+=3D=20files(=0A= +=20=20'auth-oauth.c',=0A=20=20=20'auth-sasl.c',=0A=20=20=20= 'auth-scram.c',=0A=20=20=20'auth.c',=0Adiff=20--git=20= a/src/backend/libpq/pg_hba.conf.sample=20= b/src/backend/libpq/pg_hba.conf.sample=0Aindex=20= bad13497a34..b64c8dea97c=20100644=0A---=20= a/src/backend/libpq/pg_hba.conf.sample=0A+++=20= b/src/backend/libpq/pg_hba.conf.sample=0A@@=20-53,8=20+53,8=20@@=0A=20#=20= directly=20connected=20to.=0A=20#=0A=20#=20METHOD=20can=20be=20"trust",=20= "reject",=20"md5",=20"password",=20"scram-sha-256",=0A-#=20"gss",=20= "sspi",=20"ident",=20"peer",=20"pam",=20"ldap",=20"radius"=20or=20= "cert".=0A-#=20Note=20that=20"password"=20sends=20passwords=20in=20clear=20= text;=20"md5"=20or=0A+#=20"gss",=20"sspi",=20"ident",=20"peer",=20"pam",=20= "oauth",=20"ldap",=20"radius"=20or=0A+#=20"cert".=20=20Note=20that=20= "password"=20sends=20passwords=20in=20clear=20text;=20"md5"=20or=0A=20#=20= "scram-sha-256"=20are=20preferred=20since=20they=20send=20encrypted=20= passwords.=0A=20#=0A=20#=20OPTIONS=20are=20a=20set=20of=20options=20for=20= the=20authentication=20in=20the=20format=0Adiff=20--git=20= a/src/backend/utils/adt/hbafuncs.c=20b/src/backend/utils/adt/hbafuncs.c=0A= index=2003c38e8c451..b62c3d944cf=20100644=0A---=20= a/src/backend/utils/adt/hbafuncs.c=0A+++=20= b/src/backend/utils/adt/hbafuncs.c=0A@@=20-152,6=20+152,25=20@@=20= get_hba_options(HbaLine=20*hba)=0A=20=09=09=09=09= CStringGetTextDatum(psprintf("radiusports=3D%s",=20hba->radiusports_s));=0A= =20=09}=0A=20=0A+=09if=20(hba->auth_method=20=3D=3D=20uaOAuth)=0A+=09{=0A= +=09=09if=20(hba->oauth_issuer)=0A+=09=09=09options[noptions++]=20=3D=0A= +=09=09=09=09CStringGetTextDatum(psprintf("issuer=3D%s",=20= hba->oauth_issuer));=0A+=0A+=09=09if=20(hba->oauth_scope)=0A+=09=09=09= options[noptions++]=20=3D=0A+=09=09=09=09= CStringGetTextDatum(psprintf("scope=3D%s",=20hba->oauth_scope));=0A+=0A+=09= =09if=20(hba->oauth_validator)=0A+=09=09=09options[noptions++]=20=3D=0A+=09= =09=09=09CStringGetTextDatum(psprintf("validator=3D%s",=20= hba->oauth_validator));=0A+=0A+=09=09if=20(hba->oauth_skip_usermap)=0A+=09= =09=09options[noptions++]=20=3D=0A+=09=09=09=09= CStringGetTextDatum(psprintf("delegate_ident_mapping=3Dtrue"));=0A+=09}=0A= +=0A=20=09/*=20If=20you=20add=20more=20options,=20consider=20increasing=20= MAX_HBA_OPTIONS.=20*/=0A=20=09Assert(noptions=20<=3D=20MAX_HBA_OPTIONS);=0A= =20=0Adiff=20--git=20a/src/backend/utils/misc/guc_tables.c=20= b/src/backend/utils/misc/guc_tables.c=0Aindex=20226af43fe23..68833ca5fa3=20= 100644=0A---=20a/src/backend/utils/misc/guc_tables.c=0A+++=20= b/src/backend/utils/misc/guc_tables.c=0A@@=20-49,6=20+49,7=20@@=0A=20= #include=20"jit/jit.h"=0A=20#include=20"libpq/auth.h"=0A=20#include=20= "libpq/libpq.h"=0A+#include=20"libpq/oauth.h"=0A=20#include=20= "libpq/scram.h"=0A=20#include=20"nodes/queryjumble.h"=0A=20#include=20= "optimizer/cost.h"=0A@@=20-4852,6=20+4853,17=20@@=20struct=20= config_string=20ConfigureNamesString[]=20=3D=0A=20=09=09= check_restrict_nonsystem_relation_kind,=20= assign_restrict_nonsystem_relation_kind,=20NULL=0A=20=09},=0A=20=0A+=09{=0A= +=09=09{"oauth_validator_libraries",=20PGC_SIGHUP,=20CONN_AUTH_AUTH,=0A+=09= =09=09gettext_noop("Lists=20libraries=20that=20may=20be=20called=20to=20= validate=20OAuth=20v2=20bearer=20tokens."),=0A+=09=09=09NULL,=0A+=09=09=09= GUC_LIST_INPUT=20|=20GUC_LIST_QUOTE=20|=20GUC_SUPERUSER_ONLY=0A+=09=09},=0A= +=09=09&oauth_validator_libraries_string,=0A+=09=09"",=0A+=09=09NULL,=20= NULL,=20NULL=0A+=09},=0A+=0A=20=09/*=20End-of-list=20marker=20*/=0A=20=09= {=0A=20=09=09{NULL,=200,=200,=20NULL,=20NULL},=20NULL,=20NULL,=20NULL,=20= NULL,=20NULL=0Adiff=20--git=20= a/src/backend/utils/misc/postgresql.conf.sample=20= b/src/backend/utils/misc/postgresql.conf.sample=0Aindex=20= d472987ed46..ccefd214143=20100644=0A---=20= a/src/backend/utils/misc/postgresql.conf.sample=0A+++=20= b/src/backend/utils/misc/postgresql.conf.sample=0A@@=20-121,6=20+121,9=20= @@=0A=20#ssl_passphrase_command=20=3D=20''=0A=20= #ssl_passphrase_command_supports_reload=20=3D=20off=0A=20=0A+#=20OAuth=0A= +#oauth_validator_libraries=20=3D=20''=09#=20comma-separated=20list=20of=20= trusted=20validator=20modules=0A+=0A=20=0A=20= #-------------------------------------------------------------------------= -----=0A=20#=20RESOURCE=20USAGE=20(except=20WAL)=0Adiff=20--git=20= a/src/include/common/oauth-common.h=20= b/src/include/common/oauth-common.h=0Anew=20file=20mode=20100644=0Aindex=20= 00000000000..8fe56267780=0A---=20/dev/null=0A+++=20= b/src/include/common/oauth-common.h=0A@@=20-0,0=20+1,19=20@@=0A= +/*-----------------------------------------------------------------------= --=0A+=20*=0A+=20*=20oauth-common.h=0A+=20*=09=09Declarations=20for=20= helper=20functions=20used=20for=20OAuth/OIDC=20authentication=0A+=20*=0A= +=20*=20Portions=20Copyright=20(c)=201996-2024,=20PostgreSQL=20Global=20= Development=20Group=0A+=20*=20Portions=20Copyright=20(c)=201994,=20= Regents=20of=20the=20University=20of=20California=0A+=20*=0A+=20*=20= src/include/common/oauth-common.h=0A+=20*=0A+=20= *-------------------------------------------------------------------------= =0A+=20*/=0A+#ifndef=20OAUTH_COMMON_H=0A+#define=20OAUTH_COMMON_H=0A+=0A= +/*=20Name=20of=20SASL=20mechanism=20per=20IANA=20*/=0A+#define=20= OAUTHBEARER_NAME=20"OAUTHBEARER"=0A+=0A+#endif=09=09=09=09=09=09=09/*=20= OAUTH_COMMON_H=20*/=0Adiff=20--git=20a/src/include/libpq/auth.h=20= b/src/include/libpq/auth.h=0Aindex=20902c5f6de32..25b5742068f=20100644=0A= ---=20a/src/include/libpq/auth.h=0A+++=20b/src/include/libpq/auth.h=0A@@=20= -39,6=20+39,7=20@@=20extern=20PGDLLIMPORT=20bool=20= pg_gss_accept_delegation;=0A=20extern=20void=20ClientAuthentication(Port=20= *port);=0A=20extern=20void=20sendAuthRequest(Port=20*port,=20AuthRequest=20= areq,=20const=20char=20*extradata,=0A=20=09=09=09=09=09=09=09int=20= extralen);=0A+extern=20void=20set_authn_id(Port=20*port,=20const=20char=20= *id);=0A=20=0A=20/*=20Hook=20for=20plugins=20to=20get=20control=20in=20= ClientAuthentication()=20*/=0A=20typedef=20void=20= (*ClientAuthentication_hook_type)=20(Port=20*,=20int);=0Adiff=20--git=20= a/src/include/libpq/hba.h=20b/src/include/libpq/hba.h=0Aindex=20= b20d0051f7d..3657f182db3=20100644=0A---=20a/src/include/libpq/hba.h=0A= +++=20b/src/include/libpq/hba.h=0A@@=20-39,7=20+39,8=20@@=20typedef=20= enum=20UserAuth=0A=20=09uaCert,=0A=20=09uaRADIUS,=0A=20=09uaPeer,=0A= -#define=20USER_AUTH_LAST=20uaPeer=09/*=20Must=20be=20last=20value=20of=20= this=20enum=20*/=0A+=09uaOAuth,=0A+#define=20USER_AUTH_LAST=20uaOAuth=09= /*=20Must=20be=20last=20value=20of=20this=20enum=20*/=0A=20}=20UserAuth;=0A= =20=0A=20/*=0A@@=20-135,6=20+136,10=20@@=20typedef=20struct=20HbaLine=0A=20= =09char=09=20=20=20*radiusidentifiers_s;=0A=20=09List=09=20=20=20= *radiusports;=0A=20=09char=09=20=20=20*radiusports_s;=0A+=09char=09=20=20= =20*oauth_issuer;=0A+=09char=09=20=20=20*oauth_scope;=0A+=09char=09=20=20= =20*oauth_validator;=0A+=09bool=09=09oauth_skip_usermap;=0A=20}=20= HbaLine;=0A=20=0A=20typedef=20struct=20IdentLine=0Adiff=20--git=20= a/src/include/libpq/oauth.h=20b/src/include/libpq/oauth.h=0Anew=20file=20= mode=20100644=0Aindex=2000000000000..4fcdda74305=0A---=20/dev/null=0A+++=20= b/src/include/libpq/oauth.h=0A@@=20-0,0=20+1,54=20@@=0A= +/*-----------------------------------------------------------------------= --=0A+=20*=0A+=20*=20oauth.h=0A+=20*=09=20=20Interface=20to=20= libpq/auth-oauth.c=0A+=20*=0A+=20*=20Portions=20Copyright=20(c)=20= 1996-2024,=20PostgreSQL=20Global=20Development=20Group=0A+=20*=20= Portions=20Copyright=20(c)=201994,=20Regents=20of=20the=20University=20= of=20California=0A+=20*=0A+=20*=20src/include/libpq/oauth.h=0A+=20*=0A+=20= *-------------------------------------------------------------------------= =0A+=20*/=0A+#ifndef=20PG_OAUTH_H=0A+#define=20PG_OAUTH_H=0A+=0A= +#include=20"libpq/libpq-be.h"=0A+#include=20"libpq/sasl.h"=0A+=0A= +extern=20PGDLLIMPORT=20char=20*oauth_validator_libraries_string;=0A+=0A= +typedef=20struct=20ValidatorModuleState=0A+{=0A+=09void=09=20=20=20= *private_data;=0A+}=20ValidatorModuleState;=0A+=0A+typedef=20struct=20= ValidatorModuleResult=0A+{=0A+=09bool=09=09authorized;=0A+=09char=09=20=20= =20*authn_id;=0A+}=20ValidatorModuleResult;=0A+=0A+typedef=20void=20= (*ValidatorStartupCB)=20(ValidatorModuleState=20*state);=0A+typedef=20= void=20(*ValidatorShutdownCB)=20(ValidatorModuleState=20*state);=0A= +typedef=20ValidatorModuleResult=20*(*ValidatorValidateCB)=20= (ValidatorModuleState=20*state,=20const=20char=20*token,=20const=20char=20= *role);=0A+=0A+typedef=20struct=20OAuthValidatorCallbacks=0A+{=0A+=09= ValidatorStartupCB=20startup_cb;=0A+=09ValidatorShutdownCB=20= shutdown_cb;=0A+=09ValidatorValidateCB=20validate_cb;=0A+}=20= OAuthValidatorCallbacks;=0A+=0A+typedef=20const=20= OAuthValidatorCallbacks=20*(*OAuthValidatorModuleInit)=20(void);=0A= +extern=20PGDLLEXPORT=20const=20OAuthValidatorCallbacks=20= *_PG_oauth_validator_module_init(void);=0A+=0A+/*=20Implementation=20*/=0A= +extern=20const=20pg_be_sasl_mech=20pg_be_oauth_mech;=0A+=0A+/*=0A+=20*=20= Ensure=20a=20validator=20named=20in=20the=20HBA=20is=20permitted=20by=20= the=20configuration.=0A+=20*/=0A+extern=20bool=20= check_oauth_validator(HbaLine=20*hba,=20int=20elevel,=20char=20= **err_msg);=0A+=0A+#endif=09=09=09=09=09=09=09/*=20PG_OAUTH_H=20*/=0A= diff=20--git=20a/src/include/pg_config.h.in=20= b/src/include/pg_config.h.in=0Aindex=2007b2f798abd..c04ee38d086=20100644=0A= ---=20a/src/include/pg_config.h.in=0A+++=20b/src/include/pg_config.h.in=0A= @@=20-229,6=20+229,9=20@@=0A=20/*=20Define=20to=201=20if=20you=20have=20= the=20`crypto'=20library=20(-lcrypto).=20*/=0A=20#undef=20HAVE_LIBCRYPTO=0A= =20=0A+/*=20Define=20to=201=20if=20you=20have=20the=20`curl'=20library=20= (-lcurl).=20*/=0A+#undef=20HAVE_LIBCURL=0A+=0A=20/*=20Define=20to=201=20= if=20you=20have=20the=20`ldap'=20library=20(-lldap).=20*/=0A=20#undef=20= HAVE_LIBLDAP=0A=20=0A@@=20-442,6=20+445,9=20@@=0A=20/*=20Define=20to=201=20= if=20you=20have=20the=20=20header=20file.=20*/=0A=20#undef=20= HAVE_TERMIOS_H=0A=20=0A+/*=20Define=20to=201=20if=20curl_global_init()=20= is=20guaranteed=20to=20be=20threadsafe.=20*/=0A+#undef=20= HAVE_THREADSAFE_CURL_GLOBAL_INIT=0A+=0A=20/*=20Define=20to=201=20if=20= your=20compiler=20understands=20`typeof'=20or=20something=20similar.=20= */=0A=20#undef=20HAVE_TYPEOF=0A=20=0A@@=20-663,6=20+669,9=20@@=0A=20/*=20= Define=20to=201=20to=20build=20with=20LDAP=20support.=20(--with-ldap)=20= */=0A=20#undef=20USE_LDAP=0A=20=0A+/*=20Define=20to=201=20to=20build=20= with=20libcurl=20support.=20(--with-libcurl)=20*/=0A+#undef=20= USE_LIBCURL=0A+=0A=20/*=20Define=20to=201=20to=20build=20with=20XML=20= support.=20(--with-libxml)=20*/=0A=20#undef=20USE_LIBXML=0A=20=0Adiff=20= --git=20a/src/interfaces/libpq/Makefile=20= b/src/interfaces/libpq/Makefile=0Aindex=20701810a272a..90b0b65db6f=20= 100644=0A---=20a/src/interfaces/libpq/Makefile=0A+++=20= b/src/interfaces/libpq/Makefile=0A@@=20-31,6=20+31,7=20@@=20endif=0A=20=0A= =20OBJS=20=3D=20\=0A=20=09$(WIN32RES)=20\=0A+=09fe-auth-oauth.o=20\=0A=20= =09fe-auth-scram.o=20\=0A=20=09fe-cancel.o=20\=0A=20=09fe-connect.o=20\=0A= @@=20-63,6=20+64,10=20@@=20OBJS=20+=3D=20\=0A=20=09fe-secure-gssapi.o=0A=20= endif=0A=20=0A+ifeq=20($(with_libcurl),yes)=0A+OBJS=20+=3D=20= fe-auth-oauth-curl.o=0A+endif=0A+=0A=20ifeq=20($(PORTNAME),=20cygwin)=0A=20= override=20shlib=20=3D=20cyg$(NAME)$(DLSUFFIX)=0A=20endif=0A@@=20-81,7=20= +86,7=20@@=20endif=0A=20#=20that=20are=20built=20correctly=20for=20use=20= in=20a=20shlib.=0A=20SHLIB_LINK_INTERNAL=20=3D=20-lpgcommon_shlib=20= -lpgport_shlib=0A=20ifneq=20($(PORTNAME),=20win32)=0A-SHLIB_LINK=20+=3D=20= $(filter=20-lcrypt=20-ldes=20-lcom_err=20-lcrypto=20-lk5crypto=20-lkrb5=20= -lgssapi_krb5=20-lgss=20-lgssapi=20-lssl=20-lsocket=20-lnsl=20-lresolv=20= -lintl=20-lm,=20$(LIBS))=20$(LDAP_LIBS_FE)=20$(PTHREAD_LIBS)=0A= +SHLIB_LINK=20+=3D=20$(filter=20-lcrypt=20-ldes=20-lcom_err=20-lcrypto=20= -lk5crypto=20-lkrb5=20-lgssapi_krb5=20-lgss=20-lgssapi=20-lssl=20-lcurl=20= -lsocket=20-lnsl=20-lresolv=20-lintl=20-lm,=20$(LIBS))=20$(LDAP_LIBS_FE)=20= $(PTHREAD_LIBS)=0A=20else=0A=20SHLIB_LINK=20+=3D=20$(filter=20-lcrypt=20= -ldes=20-lcom_err=20-lcrypto=20-lk5crypto=20-lkrb5=20-lgssapi32=20-lssl=20= -lsocket=20-lnsl=20-lresolv=20-lintl=20-lm=20$(PTHREAD_LIBS),=20$(LIBS))=20= $(LDAP_LIBS_FE)=0A=20endif=0A@@=20-110,6=20+115,8=20@@=20backend_src=20=3D= =20$(top_srcdir)/src/backend=0A=20#=20which=20seems=20to=20insert=20= references=20to=20that=20even=20in=20pure=20C=20code.=20Excluding=0A=20#=20= __tsan_func_exit=20is=20necessary=20when=20using=20ThreadSanitizer=20= data=20race=20detector=0A=20#=20which=20use=20this=20function=20for=20= instrumentation=20of=20function=20exit.=0A+#=20libcurl=20registers=20an=20= exit=20handler=20in=20the=20memory=20debugging=20code=20when=20running=0A= +#=20with=20LeakSanitizer.=0A=20#=20Skip=20the=20test=20when=20= profiling,=20as=20gcc=20may=20insert=20exit()=20calls=20for=20that.=0A=20= #=20Also=20skip=20the=20test=20on=20platforms=20where=20libpq=20= infrastructure=20may=20be=20provided=0A=20#=20by=20statically-linked=20= libraries,=20as=20we=20can't=20expect=20them=20to=20honor=20this=0A@@=20= -117,7=20+124,7=20@@=20backend_src=20=3D=20$(top_srcdir)/src/backend=0A=20= libpq-refs-stamp:=20$(shlib)=0A=20ifneq=20($(enable_coverage),=20yes)=0A=20= ifeq=20(,$(filter=20solaris,$(PORTNAME)))=0A-=09@if=20nm=20-A=20-u=20$<=20= 2>/dev/null=20|=20grep=20-v=20-e=20__cxa_atexit=20-e=20__tsan_func_exit=20= |=20grep=20exit;=20then=20\=0A+=09@if=20nm=20-A=20-u=20$<=202>/dev/null=20= |=20grep=20-v=20-e=20__cxa_atexit=20-e=20__tsan_func_exit=20-e=20_atexit=20= |=20grep=20exit;=20then=20\=0A=20=09=09echo=20'libpq=20must=20not=20be=20= calling=20any=20function=20which=20invokes=20exit';=20exit=201;=20\=0A=20= =09fi=0A=20endif=0Adiff=20--git=20a/src/interfaces/libpq/exports.txt=20= b/src/interfaces/libpq/exports.txt=0Aindex=202ad2cbf5ca3..9b789cbec0b=20= 100644=0A---=20a/src/interfaces/libpq/exports.txt=0A+++=20= b/src/interfaces/libpq/exports.txt=0A@@=20-206,3=20+206,6=20@@=20= PQsocketPoll=20=20=20=20=20=20=20=20=20=20=20=20=20=20203=0A=20= PQsetChunkedRowsMode=20=20=20=20=20=20204=0A=20PQgetCurrentTimeUSec=20=20= =20=20=20=20205=0A=20PQservice=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20206=0A+PQsetAuthDataHook=20=20=20=20=20=20=20=20=20207=0A= +PQgetAuthDataHook=20=20=20=20=20=20=20=20=20208=0A= +PQdefaultAuthDataHook=20=20=20=20=20209=0Adiff=20--git=20= a/src/interfaces/libpq/fe-auth-oauth-curl.c=20= b/src/interfaces/libpq/fe-auth-oauth-curl.c=0Anew=20file=20mode=20100644=0A= index=2000000000000..2179bb89800=0A---=20/dev/null=0A+++=20= b/src/interfaces/libpq/fe-auth-oauth-curl.c=0A@@=20-0,0=20+1,2858=20@@=0A= +/*-----------------------------------------------------------------------= --=0A+=20*=0A+=20*=20fe-auth-oauth-curl.c=0A+=20*=09=20=20=20The=20= libcurl=20implementation=20of=20OAuth/OIDC=20authentication,=20using=20= the=0A+=20*=09=20=20=20OAuth=20Device=20Authorization=20Grant=20(RFC=20= 8628).=0A+=20*=0A+=20*=20Portions=20Copyright=20(c)=201996-2024,=20= PostgreSQL=20Global=20Development=20Group=0A+=20*=20Portions=20Copyright=20= (c)=201994,=20Regents=20of=20the=20University=20of=20California=0A+=20*=0A= +=20*=20IDENTIFICATION=0A+=20*=09=20=20= src/interfaces/libpq/fe-auth-oauth-curl.c=0A+=20*=0A+=20= *-------------------------------------------------------------------------= =0A+=20*/=0A+=0A+#include=20"postgres_fe.h"=0A+=0A+#include=20= =0A+#include=20=0A+#ifdef=20HAVE_SYS_EPOLL_H=0A= +#include=20=0A+#include=20=0A+#endif=0A= +#ifdef=20HAVE_SYS_EVENT_H=0A+#include=20=0A+#endif=0A= +#include=20=0A+=0A+#include=20"common/jsonapi.h"=0A+#include=20= "fe-auth.h"=0A+#include=20"fe-auth-oauth.h"=0A+#include=20"libpq-int.h"=0A= +#include=20"mb/pg_wchar.h"=0A+=0A+#define=20MAX_OAUTH_RESPONSE_SIZE=20= (1024=20*=201024)=0A+=0A+/*=0A+=20*=20Parsed=20JSON=20Representations=0A= +=20*=0A+=20*=20As=20a=20general=20rule,=20we=20parse=20and=20cache=20= only=20the=20fields=20we're=20currently=20using.=0A+=20*=20When=20adding=20= new=20fields,=20ensure=20the=20corresponding=20free_*()=20function=20is=20= updated=0A+=20*=20too.=0A+=20*/=0A+=0A+/*=0A+=20*=20The=20OpenID=20= Provider=20configuration=20(alternatively=20named=20"authorization=20= server=0A+=20*=20metadata")=20jointly=20described=20by=20OpenID=20= Connect=20Discovery=201.0=20and=20RFC=208414:=0A+=20*=0A+=20*=20=20=20=20= =20https://openid.net/specs/openid-connect-discovery-1_0.html=0A+=20*=20=20= =20=20=20https://www.rfc-editor.org/rfc/rfc8414#section-3.2=0A+=20*/=0A= +struct=20provider=0A+{=0A+=09char=09=20=20=20*issuer;=0A+=09char=09=20=20= =20*token_endpoint;=0A+=09char=09=20=20=20= *device_authorization_endpoint;=0A+=09struct=20curl_slist=20= *grant_types_supported;=0A+};=0A+=0A+static=20void=0A= +free_provider(struct=20provider=20*provider)=0A+{=0A+=09= free(provider->issuer);=0A+=09free(provider->token_endpoint);=0A+=09= free(provider->device_authorization_endpoint);=0A+=09= curl_slist_free_all(provider->grant_types_supported);=0A+}=0A+=0A+/*=0A+=20= *=20The=20Device=20Authorization=20response,=20described=20by=20RFC=20= 8628:=0A+=20*=0A+=20*=20=20=20=20=20= https://www.rfc-editor.org/rfc/rfc8628#section-3.2=0A+=20*/=0A+struct=20= device_authz=0A+{=0A+=09char=09=20=20=20*device_code;=0A+=09char=09=20=20= =20*user_code;=0A+=09char=09=20=20=20*verification_uri;=0A+=09char=09=20=20= =20*verification_uri_complete;=0A+=09char=09=20=20=20*expires_in_str;=0A= +=09char=09=20=20=20*interval_str;=0A+=0A+=09/*=20Fields=20below=20are=20= parsed=20from=20the=20corresponding=20string=20above.=20*/=0A+=09int=09=09= =09expires_in;=0A+=09int=09=09=09interval;=0A+};=0A+=0A+static=20void=0A= +free_device_authz(struct=20device_authz=20*authz)=0A+{=0A+=09= free(authz->device_code);=0A+=09free(authz->user_code);=0A+=09= free(authz->verification_uri);=0A+=09= free(authz->verification_uri_complete);=0A+=09= free(authz->expires_in_str);=0A+=09free(authz->interval_str);=0A+}=0A+=0A= +/*=0A+=20*=20The=20Token=20Endpoint=20error=20response,=20as=20= described=20by=20RFC=206749:=0A+=20*=0A+=20*=20=20=20=20=20= https://www.rfc-editor.org/rfc/rfc6749#section-5.2=0A+=20*=0A+=20*=20= Note=20that=20this=20response=20type=20can=20also=20be=20returned=20from=20= the=20Device=0A+=20*=20Authorization=20Endpoint.=0A+=20*/=0A+struct=20= token_error=0A+{=0A+=09char=09=20=20=20*error;=0A+=09char=09=20=20=20= *error_description;=0A+};=0A+=0A+static=20void=0A= +free_token_error(struct=20token_error=20*err)=0A+{=0A+=09= free(err->error);=0A+=09free(err->error_description);=0A+}=0A+=0A+/*=0A+=20= *=20The=20Access=20Token=20response,=20as=20described=20by=20RFC=206749:=0A= +=20*=0A+=20*=20=20=20=20=20= https://www.rfc-editor.org/rfc/rfc6749#section-4.1.4=0A+=20*=0A+=20*=20= During=20the=20Device=20Authorization=20flow,=20several=20temporary=20= errors=20are=20expected=0A+=20*=20as=20part=20of=20normal=20operation.=20= To=20make=20it=20easy=20to=20handle=20these=20in=20the=20happy=0A+=20*=20= path,=20this=20contains=20an=20embedded=20token_error=20that=20is=20= filled=20in=20if=20needed.=0A+=20*/=0A+struct=20token=0A+{=0A+=09/*=20= for=20successful=20responses=20*/=0A+=09char=09=20=20=20*access_token;=0A= +=09char=09=20=20=20*token_type;=0A+=0A+=09/*=20for=20error=20responses=20= */=0A+=09struct=20token_error=20err;=0A+};=0A+=0A+static=20void=0A= +free_token(struct=20token=20*tok)=0A+{=0A+=09free(tok->access_token);=0A= +=09free(tok->token_type);=0A+=09free_token_error(&tok->err);=0A+}=0A+=0A= +/*=0A+=20*=20Asynchronous=20State=0A+=20*/=0A+=0A+/*=20States=20for=20= the=20overall=20async=20machine.=20*/=0A+enum=20OAuthStep=0A+{=0A+=09= OAUTH_STEP_INIT=20=3D=200,=0A+=09OAUTH_STEP_DISCOVERY,=0A+=09= OAUTH_STEP_DEVICE_AUTHORIZATION,=0A+=09OAUTH_STEP_TOKEN_REQUEST,=0A+=09= OAUTH_STEP_WAIT_INTERVAL,=0A+};=0A+=0A+/*=0A+=20*=20The=20async_ctx=20= holds=20onto=20state=20that=20needs=20to=20persist=20across=20multiple=20= calls=0A+=20*=20to=20pg_fe_run_oauth_flow().=20Almost=20everything=20= interacts=20with=20this=20in=20some=0A+=20*=20way.=0A+=20*/=0A+struct=20= async_ctx=0A+{=0A+=09enum=20OAuthStep=20step;=09=09/*=20where=20are=20we=20= in=20the=20flow?=20*/=0A+=0A+=09int=09=09=09timerfd;=09=09/*=20= descriptor=20for=20signaling=20async=20timeouts=20*/=0A+=09pgsocket=09= mux;=09=09=09/*=20the=20multiplexer=20socket=20containing=20all=0A+=09=09= =09=09=09=09=09=09=20*=20descriptors=20tracked=20by=20libcurl,=20plus=20= the=0A+=09=09=09=09=09=09=09=09=20*=20timerfd=20*/=0A+=09CURLM=09=20=20=20= *curlm;=09=09=09/*=20top-level=20multi=20handle=20for=20libcurl=0A+=09=09= =09=09=09=09=09=09=20*=20operations=20*/=0A+=09CURL=09=20=20=20*curl;=09=09= =09/*=20the=20(single)=20easy=20handle=20for=20serial=0A+=09=09=09=09=09=09= =09=09=20*=20requests=20*/=0A+=0A+=09struct=20curl_slist=20*headers;=20= /*=20common=20headers=20for=20all=20requests=20*/=0A+=09PQExpBufferData=20= work_data;=09/*=20scratch=20buffer=20for=20general=20use=20(remember=20= to=0A+=09=09=09=09=09=09=09=09=20*=20clear=20out=20prior=20contents=20= first!)=20*/=0A+=0A+=09/*------=0A+=09=20*=20Since=20a=20single=20= logical=20operation=20may=20stretch=20across=20multiple=20calls=20to=0A+=09= =20*=20our=20entry=20point,=20errors=20have=20three=20parts:=0A+=09=20*=0A= +=09=20*=20-=20errctx:=09an=20optional=20static=20string,=20describing=20= the=20global=20operation=0A+=09=20*=09=09=09=09currently=20in=20= progress.=20It'll=20be=20translated=20for=20you.=0A+=09=20*=0A+=09=20*=20= -=20errbuf:=09contains=20the=20actual=20error=20message.=20Generally=20= speaking,=20use=0A+=09=20*=09=09=09=09actx_error[_str]=20to=20manipulate=20= this.=20This=20must=20be=20filled=0A+=09=20*=09=09=09=09with=20something=20= useful=20on=20an=20error.=0A+=09=20*=0A+=09=20*=20-=20curl_err:=09an=20= optional=20static=20error=20buffer=20used=20by=20libcurl=20to=20put=0A+=09= =20*=09=09=09=09detailed=20information=20about=20failures.=20= Unfortunately=0A+=09=20*=09=09=09=09untranslatable.=0A+=09=20*=0A+=09=20= *=20These=20pieces=20will=20be=20combined=20into=20a=20single=20error=20= message=20looking=0A+=09=20*=20something=20like=20the=20following,=20= with=20errctx=20and/or=20curl_err=20omitted=20when=0A+=09=20*=20absent:=0A= +=09=20*=0A+=09=20*=20=20=20=20=20connection=20to=20server=20...=20= failed:=20errctx:=20errbuf=20(curl_err)=0A+=09=20*/=0A+=09const=20char=20= *errctx;=09=09=09/*=20not=20freed;=20must=20point=20to=20static=20= allocation=20*/=0A+=09PQExpBufferData=20errbuf;=0A+=09char=09=09= curl_err[CURL_ERROR_SIZE];=0A+=0A+=09/*=0A+=09=20*=20These=20documents=20= need=20to=20survive=20over=20multiple=20calls,=20and=20are=20therefore=0A= +=09=20*=20cached=20directly=20in=20the=20async_ctx.=0A+=09=20*/=0A+=09= struct=20provider=20provider;=0A+=09struct=20device_authz=20authz;=0A+=0A= +=09int=09=09=09running;=09=09/*=20is=20asynchronous=20work=20in=20= progress?=20*/=0A+=09bool=09=09user_prompted;=09/*=20have=20we=20already=20= sent=20the=20authz=20prompt?=20*/=0A+=09bool=09=09used_basic_auth;=09/*=20= did=20we=20send=20a=20client=20secret?=20*/=0A+=09bool=09=09debugging;=09= =09/*=20can=20we=20give=20unsafe=20developer=20assistance?=20*/=0A+};=0A= +=0A+/*=0A+=20*=20Tears=20down=20the=20Curl=20handles=20and=20frees=20= the=20async_ctx.=0A+=20*/=0A+static=20void=0A+free_async_ctx(PGconn=20= *conn,=20struct=20async_ctx=20*actx)=0A+{=0A+=09/*=0A+=09=20*=20In=20= general,=20none=20of=20the=20error=20cases=20below=20should=20ever=20= happen=20if=20we=20have=0A+=09=20*=20no=20bugs=20above.=20But=20if=20we=20= do=20hit=20them,=20surfacing=20those=20errors=20somehow=0A+=09=20*=20= might=20be=20the=20only=20way=20to=20have=20a=20chance=20to=20debug=20= them.=0A+=09=20*=0A+=09=20*=20TODO:=20At=20some=20point=20it'd=20be=20= nice=20to=20have=20a=20standard=20way=20to=20warn=20about=0A+=09=20*=20= teardown=20failures.=20Appending=20to=20the=20connection's=20error=20= message=20only=0A+=09=20*=20helps=20if=20the=20bug=20caused=20a=20= connection=20failure;=20otherwise=20it'll=20be=0A+=09=20*=20buried...=0A= +=09=20*/=0A+=0A+=09if=20(actx->curlm=20&&=20actx->curl)=0A+=09{=0A+=09=09= CURLMcode=09err=20=3D=20curl_multi_remove_handle(actx->curlm,=20= actx->curl);=0A+=0A+=09=09if=20(err)=0A+=09=09=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09=09"libcurl=20= easy=20handle=20removal=20failed:=20%s",=0A+=09=09=09=09=09=09=09=09=09= curl_multi_strerror(err));=0A+=09}=0A+=0A+=09if=20(actx->curl)=0A+=09{=0A= +=09=09/*=0A+=09=09=20*=20curl_multi_cleanup()=20doesn't=20free=20any=20= associated=20easy=20handles;=20we=0A+=09=09=20*=20need=20to=20do=20that=20= separately.=20We=20only=20ever=20have=20one=20easy=20handle=20per=0A+=09=09= =20*=20multi=20handle.=0A+=09=09=20*/=0A+=09=09= curl_easy_cleanup(actx->curl);=0A+=09}=0A+=0A+=09if=20(actx->curlm)=0A+=09= {=0A+=09=09CURLMcode=09err=20=3D=20curl_multi_cleanup(actx->curlm);=0A+=0A= +=09=09if=20(err)=0A+=09=09=09libpq_append_conn_error(conn,=0A+=09=09=09=09= =09=09=09=09=09"libcurl=20multi=20handle=20cleanup=20failed:=20%s",=0A+=09= =09=09=09=09=09=09=09=09curl_multi_strerror(err));=0A+=09}=0A+=0A+=09= free_provider(&actx->provider);=0A+=09free_device_authz(&actx->authz);=0A= +=0A+=09curl_slist_free_all(actx->headers);=0A+=09= termPQExpBuffer(&actx->work_data);=0A+=09termPQExpBuffer(&actx->errbuf);=0A= +=0A+=09if=20(actx->mux=20!=3D=20PGINVALID_SOCKET)=0A+=09=09= close(actx->mux);=0A+=09if=20(actx->timerfd=20>=3D=200)=0A+=09=09= close(actx->timerfd);=0A+=0A+=09free(actx);=0A+}=0A+=0A+/*=0A+=20*=20= Release=20resources=20used=20for=20the=20asynchronous=20exchange=20and=20= disconnect=20the=0A+=20*=20altsock.=0A+=20*=0A+=20*=20This=20is=20called=20= either=20at=20the=20end=20of=20a=20successful=20authentication,=20or=20= during=0A+=20*=20pqDropConnection(),=20so=20we=20won't=20leak=20= resources=20even=20if=20PQconnectPoll()=20never=0A+=20*=20calls=20us=20= back.=0A+=20*/=0A+void=0A+pg_fe_cleanup_oauth_flow(PGconn=20*conn)=0A+{=0A= +=09fe_oauth_state=20*state=20=3D=20conn->sasl_state;=0A+=0A+=09if=20= (state->async_ctx)=0A+=09{=0A+=09=09free_async_ctx(conn,=20= state->async_ctx);=0A+=09=09state->async_ctx=20=3D=20NULL;=0A+=09}=0A+=0A= +=09conn->altsock=20=3D=20PGINVALID_SOCKET;=0A+}=0A+=0A+/*=0A+=20*=20= Macros=20for=20manipulating=20actx->errbuf.=20actx_error()=20translates=20= and=20formats=20a=0A+=20*=20string=20for=20you;=20actx_error_str()=20= appends=20a=20string=20directly=20without=0A+=20*=20translation.=0A+=20= */=0A+=0A+#define=20actx_error(ACTX,=20FMT,=20...)=20\=0A+=09= appendPQExpBuffer(&(ACTX)->errbuf,=20libpq_gettext(FMT),=20= ##__VA_ARGS__)=0A+=0A+#define=20actx_error_str(ACTX,=20S)=20\=0A+=09= appendPQExpBufferStr(&(ACTX)->errbuf,=20S)=0A+=0A+/*=0A+=20*=20Macros=20= for=20getting=20and=20setting=20state=20for=20the=20connection's=20two=20= libcurl=0A+=20*=20handles,=20so=20you=20don't=20have=20to=20write=20out=20= the=20error=20handling=20every=20time.=0A+=20*/=0A+=0A+#define=20= CHECK_MSETOPT(ACTX,=20OPT,=20VAL,=20FAILACTION)=20\=0A+=09do=20{=20\=0A+=09= =09struct=20async_ctx=20*_actx=20=3D=20(ACTX);=20\=0A+=09=09CURLMcode=09= _setopterr=20=3D=20curl_multi_setopt(_actx->curlm,=20OPT,=20VAL);=20\=0A= +=09=09if=20(_setopterr)=20{=20\=0A+=09=09=09actx_error(_actx,=20"failed=20= to=20set=20%s=20on=20OAuth=20connection:=20%s",\=0A+=09=09=09=09=09=20=20= =20#OPT,=20curl_multi_strerror(_setopterr));=20\=0A+=09=09=09FAILACTION;=20= \=0A+=09=09}=20\=0A+=09}=20while=20(0)=0A+=0A+#define=20= CHECK_SETOPT(ACTX,=20OPT,=20VAL,=20FAILACTION)=20\=0A+=09do=20{=20\=0A+=09= =09struct=20async_ctx=20*_actx=20=3D=20(ACTX);=20\=0A+=09=09CURLcode=09= _setopterr=20=3D=20curl_easy_setopt(_actx->curl,=20OPT,=20VAL);=20\=0A+=09= =09if=20(_setopterr)=20{=20\=0A+=09=09=09actx_error(_actx,=20"failed=20= to=20set=20%s=20on=20OAuth=20connection:=20%s",\=0A+=09=09=09=09=09=20=20= =20#OPT,=20curl_easy_strerror(_setopterr));=20\=0A+=09=09=09FAILACTION;=20= \=0A+=09=09}=20\=0A+=09}=20while=20(0)=0A+=0A+#define=20= CHECK_GETINFO(ACTX,=20INFO,=20OUT,=20FAILACTION)=20\=0A+=09do=20{=20\=0A= +=09=09struct=20async_ctx=20*_actx=20=3D=20(ACTX);=20\=0A+=09=09CURLcode=09= _getinfoerr=20=3D=20curl_easy_getinfo(_actx->curl,=20INFO,=20OUT);=20\=0A= +=09=09if=20(_getinfoerr)=20{=20\=0A+=09=09=09actx_error(_actx,=20= "failed=20to=20get=20%s=20from=20OAuth=20response:=20%s",\=0A+=09=09=09=09= =09=20=20=20#INFO,=20curl_easy_strerror(_getinfoerr));=20\=0A+=09=09=09= FAILACTION;=20\=0A+=09=09}=20\=0A+=09}=20while=20(0)=0A+=0A+/*=0A+=20*=20= General=20JSON=20Parsing=20for=20OAuth=20Responses=0A+=20*/=0A+=0A+/*=0A= +=20*=20Represents=20a=20single=20name/value=20pair=20in=20a=20JSON=20= object.=20This=20is=20the=20primary=0A+=20*=20interface=20to=20= parse_oauth_json().=0A+=20*=0A+=20*=20All=20fields=20are=20stored=20= internally=20as=20strings=20or=20lists=20of=20strings,=20so=20clients=0A= +=20*=20have=20to=20explicitly=20parse=20other=20scalar=20types=20= (though=20they=20will=20have=20gone=0A+=20*=20through=20basic=20lexical=20= validation).=20Storing=20nested=20objects=20is=20not=20currently=0A+=20*=20= supported,=20nor=20is=20parsing=20arrays=20of=20anything=20other=20than=20= strings.=0A+=20*/=0A+struct=20json_field=0A+{=0A+=09const=20char=20= *name;=09=09=09/*=20name=20(key)=20of=20the=20member=20*/=0A+=0A+=09= JsonTokenType=20type;=09=09=09/*=20currently=20supports=20= JSON_TOKEN_STRING,=0A+=09=09=09=09=09=09=09=09=20*=20JSON_TOKEN_NUMBER,=20= and=0A+=09=09=09=09=09=09=09=09=20*=20JSON_TOKEN_ARRAY_START=20*/=0A+=09= union=0A+=09{=0A+=09=09char=09=20=20**scalar;=09=09/*=20for=20all=20= scalar=20types=20*/=0A+=09=09struct=20curl_slist=20**array;=09/*=20for=20= type=20=3D=3D=20JSON_TOKEN_ARRAY_START=20*/=0A+=09}=09=09=09target;=0A+=0A= +=09bool=09=09required;=09=09/*=20REQUIRED=20field,=20or=20just=20= OPTIONAL?=20*/=0A+};=0A+=0A+/*=20Documentation=20macros=20for=20= json_field.required.=20*/=0A+#define=20REQUIRED=20true=0A+#define=20= OPTIONAL=20false=0A+=0A+/*=20Parse=20state=20for=20parse_oauth_json().=20= */=0A+struct=20oauth_parse=0A+{=0A+=09PQExpBuffer=20errbuf;=09=09=09/*=20= detail=20message=20for=20JSON_SEM_ACTION_FAILED=20*/=0A+=09int=09=09=09= nested;=09=09=09/*=20nesting=20level=20(zero=20is=20the=20top)=20*/=0A+=0A= +=09const=20struct=20json_field=20*fields;=09/*=20field=20definition=20= array=20*/=0A+=09const=20struct=20json_field=20*active;=09/*=20points=20= inside=20the=20fields=20array=20*/=0A+};=0A+=0A+#define=20= oauth_parse_set_error(ctx,=20fmt,=20...)=20\=0A+=09= appendPQExpBuffer((ctx)->errbuf,=20libpq_gettext(fmt),=20##__VA_ARGS__)=0A= +=0A+static=20void=0A+report_type_mismatch(struct=20oauth_parse=20*ctx)=0A= +{=0A+=09char=09=20=20=20*msgfmt;=0A+=0A+=09Assert(ctx->active);=0A+=0A+=09= /*=0A+=09=20*=20At=20the=20moment,=20the=20only=20fields=20we're=20= interested=20in=20are=20strings,=0A+=09=20*=20numbers,=20and=20arrays=20= of=20strings.=0A+=09=20*/=0A+=09switch=20(ctx->active->type)=0A+=09{=0A+=09= =09case=20JSON_TOKEN_STRING:=0A+=09=09=09msgfmt=20=3D=20"field=20\"%s\"=20= must=20be=20a=20string";=0A+=09=09=09break;=0A+=0A+=09=09case=20= JSON_TOKEN_NUMBER:=0A+=09=09=09msgfmt=20=3D=20"field=20\"%s\"=20must=20= be=20a=20number";=0A+=09=09=09break;=0A+=0A+=09=09case=20= JSON_TOKEN_ARRAY_START:=0A+=09=09=09msgfmt=20=3D=20"field=20\"%s\"=20= must=20be=20an=20array=20of=20strings";=0A+=09=09=09break;=0A+=0A+=09=09= default:=0A+=09=09=09Assert(false);=0A+=09=09=09msgfmt=20=3D=20"field=20= \"%s\"=20has=20unexpected=20type";=0A+=09}=0A+=0A+=09= oauth_parse_set_error(ctx,=20msgfmt,=20ctx->active->name);=0A+}=0A+=0A= +static=20JsonParseErrorType=0A+oauth_json_object_start(void=20*state)=0A= +{=0A+=09struct=20oauth_parse=20*ctx=20=3D=20state;=0A+=0A+=09if=20= (ctx->active)=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20Currently,=20none=20of=20= the=20fields=20we're=20interested=20in=20can=20be=20or=20contain=0A+=09=09= =20*=20objects,=20so=20we=20can=20reject=20this=20case=20outright.=0A+=09= =09=20*/=0A+=09=09report_type_mismatch(ctx);=0A+=09=09return=20= JSON_SEM_ACTION_FAILED;=0A+=09}=0A+=0A+=09++ctx->nested;=0A+=09return=20= JSON_SUCCESS;=0A+}=0A+=0A+static=20JsonParseErrorType=0A= +oauth_json_object_field_start(void=20*state,=20char=20*name,=20bool=20= isnull)=0A+{=0A+=09struct=20oauth_parse=20*ctx=20=3D=20state;=0A+=0A+=09= /*=20We=20care=20only=20about=20the=20top-level=20fields.=20*/=0A+=09if=20= (ctx->nested=20=3D=3D=201)=0A+=09{=0A+=09=09const=20struct=20json_field=20= *field=20=3D=20ctx->fields;=0A+=0A+=09=09/*=0A+=09=09=20*=20We=20should=20= never=20start=20parsing=20a=20new=20field=20while=20a=20previous=20one=20= is=0A+=09=09=20*=20still=20active.=0A+=09=09=20*/=0A+=09=09if=20= (ctx->active)=0A+=09=09{=0A+=09=09=09Assert(false);=0A+=09=09=09= oauth_parse_set_error(ctx,=0A+=09=09=09=09=09=09=09=09=20=20"internal=20= error:=20started=20field=20'%s'=20before=20field=20'%s'=20was=20= finished",=0A+=09=09=09=09=09=09=09=09=20=20name,=20ctx->active->name);=0A= +=09=09=09return=20JSON_SEM_ACTION_FAILED;=0A+=09=09}=0A+=0A+=09=09while=20= (field->name)=0A+=09=09{=0A+=09=09=09if=20(strcmp(name,=20field->name)=20= =3D=3D=200)=0A+=09=09=09{=0A+=09=09=09=09ctx->active=20=3D=20field;=0A+=09= =09=09=09break;=0A+=09=09=09}=0A+=0A+=09=09=09++field;=0A+=09=09}=0A+=0A= +=09=09/*=0A+=09=09=20*=20We=20don't=20allow=20duplicate=20field=20= names;=20error=20out=20if=20the=20target=20has=0A+=09=09=20*=20already=20= been=20set.=0A+=09=09=20*/=0A+=09=09if=20(ctx->active)=0A+=09=09{=0A+=09=09= =09field=20=3D=20ctx->active;=0A+=0A+=09=09=09if=20((field->type=20=3D=3D=20= JSON_TOKEN_ARRAY_START=20&&=20*field->target.array)=0A+=09=09=09=09||=20= (field->type=20!=3D=20JSON_TOKEN_ARRAY_START=20&&=20= *field->target.scalar))=0A+=09=09=09{=0A+=09=09=09=09= oauth_parse_set_error(ctx,=20"field=20\"%s\"=20is=20duplicated",=0A+=09=09= =09=09=09=09=09=09=09=20=20field->name);=0A+=09=09=09=09return=20= JSON_SEM_ACTION_FAILED;=0A+=09=09=09}=0A+=09=09}=0A+=09}=0A+=0A+=09= return=20JSON_SUCCESS;=0A+}=0A+=0A+static=20JsonParseErrorType=0A= +oauth_json_object_end(void=20*state)=0A+{=0A+=09struct=20oauth_parse=20= *ctx=20=3D=20state;=0A+=0A+=09--ctx->nested;=0A+=0A+=09/*=0A+=09=20*=20= All=20fields=20should=20be=20fully=20processed=20by=20the=20end=20of=20= the=20top-level=0A+=09=20*=20object.=0A+=09=20*/=0A+=09if=20= (!ctx->nested=20&&=20ctx->active)=0A+=09{=0A+=09=09Assert(false);=0A+=09=09= oauth_parse_set_error(ctx,=0A+=09=09=09=09=09=09=09=20=20"internal=20= error:=20field=20'%s'=20still=20active=20at=20end=20of=20object",=0A+=09=09= =09=09=09=09=09=20=20ctx->active->name);=0A+=09=09return=20= JSON_SEM_ACTION_FAILED;=0A+=09}=0A+=0A+=09return=20JSON_SUCCESS;=0A+}=0A= +=0A+static=20JsonParseErrorType=0A+oauth_json_array_start(void=20= *state)=0A+{=0A+=09struct=20oauth_parse=20*ctx=20=3D=20state;=0A+=0A+=09= if=20(!ctx->nested)=0A+=09{=0A+=09=09oauth_parse_set_error(ctx,=20= "top-level=20element=20must=20be=20an=20object");=0A+=09=09return=20= JSON_SEM_ACTION_FAILED;=0A+=09}=0A+=0A+=09if=20(ctx->active)=0A+=09{=0A+=09= =09if=20(ctx->active->type=20!=3D=20JSON_TOKEN_ARRAY_START=0A+=09=09/*=20= The=20arrays=20we=20care=20about=20must=20not=20have=20arrays=20as=20= values.=20*/=0A+=09=09=09||=20ctx->nested=20>=201)=0A+=09=09{=0A+=09=09=09= report_type_mismatch(ctx);=0A+=09=09=09return=20JSON_SEM_ACTION_FAILED;=0A= +=09=09}=0A+=09}=0A+=0A+=09++ctx->nested;=0A+=09return=20JSON_SUCCESS;=0A= +}=0A+=0A+static=20JsonParseErrorType=0A+oauth_json_array_end(void=20= *state)=0A+{=0A+=09struct=20oauth_parse=20*ctx=20=3D=20state;=0A+=0A+=09= if=20(ctx->active)=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20Clear=20the=20= target=20(which=20should=20be=20an=20array=20inside=20the=20top-level=0A= +=09=09=20*=20object).=20For=20this=20to=20be=20safe,=20no=20target=20= arrays=20can=20contain=20other=0A+=09=09=20*=20arrays;=20we=20check=20= for=20that=20in=20the=20array_start=20callback.=0A+=09=09=20*/=0A+=09=09= if=20(ctx->nested=20!=3D=202=20||=20ctx->active->type=20!=3D=20= JSON_TOKEN_ARRAY_START)=0A+=09=09{=0A+=09=09=09Assert(false);=0A+=09=09=09= oauth_parse_set_error(ctx,=0A+=09=09=09=09=09=09=09=09=20=20"internal=20= error:=20found=20unexpected=20array=20end=20while=20parsing=20field=20= '%s'",=0A+=09=09=09=09=09=09=09=09=20=20ctx->active->name);=0A+=09=09=09= return=20JSON_SEM_ACTION_FAILED;=0A+=09=09}=0A+=0A+=09=09ctx->active=20=3D= =20NULL;=0A+=09}=0A+=0A+=09--ctx->nested;=0A+=09return=20JSON_SUCCESS;=0A= +}=0A+=0A+static=20JsonParseErrorType=0A+oauth_json_scalar(void=20= *state,=20char=20*token,=20JsonTokenType=20type)=0A+{=0A+=09struct=20= oauth_parse=20*ctx=20=3D=20state;=0A+=0A+=09if=20(!ctx->nested)=0A+=09{=0A= +=09=09oauth_parse_set_error(ctx,=20"top-level=20element=20must=20be=20= an=20object");=0A+=09=09return=20JSON_SEM_ACTION_FAILED;=0A+=09}=0A+=0A+=09= if=20(ctx->active)=0A+=09{=0A+=09=09const=20struct=20json_field=20*field=20= =3D=20ctx->active;=0A+=09=09JsonTokenType=20expected=20=3D=20= field->type;=0A+=0A+=09=09/*=20Make=20sure=20this=20matches=20what=20the=20= active=20field=20expects.=20*/=0A+=09=09if=20(expected=20=3D=3D=20= JSON_TOKEN_ARRAY_START)=0A+=09=09{=0A+=09=09=09/*=20Are=20we=20actually=20= inside=20an=20array?=20*/=0A+=09=09=09if=20(ctx->nested=20<=202)=0A+=09=09= =09{=0A+=09=09=09=09report_type_mismatch(ctx);=0A+=09=09=09=09return=20= JSON_SEM_ACTION_FAILED;=0A+=09=09=09}=0A+=0A+=09=09=09/*=20Currently,=20= arrays=20can=20only=20contain=20strings.=20*/=0A+=09=09=09expected=20=3D=20= JSON_TOKEN_STRING;=0A+=09=09}=0A+=0A+=09=09if=20(type=20!=3D=20expected)=0A= +=09=09{=0A+=09=09=09report_type_mismatch(ctx);=0A+=09=09=09return=20= JSON_SEM_ACTION_FAILED;=0A+=09=09}=0A+=0A+=09=09if=20(field->type=20!=3D=20= JSON_TOKEN_ARRAY_START)=0A+=09=09{=0A+=09=09=09/*=20Ensure=20that=20= we're=20parsing=20the=20top-level=20keys...=20*/=0A+=09=09=09if=20= (ctx->nested=20!=3D=201)=0A+=09=09=09{=0A+=09=09=09=09Assert(false);=0A+=09= =09=09=09oauth_parse_set_error(ctx,=0A+=09=09=09=09=09=09=09=09=09=20=20= "internal=20error:=20scalar=20target=20found=20at=20nesting=20level=20= %d",=0A+=09=09=09=09=09=09=09=09=09=20=20ctx->nested);=0A+=09=09=09=09= return=20JSON_SEM_ACTION_FAILED;=0A+=09=09=09}=0A+=0A+=09=09=09/*=20= ...and=20that=20a=20result=20has=20not=20already=20been=20set.=20*/=0A+=09= =09=09if=20(*field->target.scalar)=0A+=09=09=09{=0A+=09=09=09=09= Assert(false);=0A+=09=09=09=09oauth_parse_set_error(ctx,=0A+=09=09=09=09=09= =09=09=09=09=20=20"internal=20error:=20scalar=20field=20'%s'=20would=20= be=20assigned=20twice",=0A+=09=09=09=09=09=09=09=09=09=20=20= ctx->active->name);=0A+=09=09=09=09return=20JSON_SEM_ACTION_FAILED;=0A+=09= =09=09}=0A+=0A+=09=09=09*field->target.scalar=20=3D=20strdup(token);=0A+=09= =09=09if=20(!*field->target.scalar)=0A+=09=09=09=09return=20= JSON_OUT_OF_MEMORY;=0A+=0A+=09=09=09ctx->active=20=3D=20NULL;=0A+=0A+=09=09= =09return=20JSON_SUCCESS;=0A+=09=09}=0A+=09=09else=0A+=09=09{=0A+=09=09=09= struct=20curl_slist=20*temp;=0A+=0A+=09=09=09/*=20The=20target=20array=20= should=20be=20inside=20the=20top-level=20object.=20*/=0A+=09=09=09if=20= (ctx->nested=20!=3D=202)=0A+=09=09=09{=0A+=09=09=09=09Assert(false);=0A+=09= =09=09=09oauth_parse_set_error(ctx,=0A+=09=09=09=09=09=09=09=09=09=20=20= "internal=20error:=20array=20member=20found=20at=20nesting=20level=20= %d",=0A+=09=09=09=09=09=09=09=09=09=20=20ctx->nested);=0A+=09=09=09=09= return=20JSON_SEM_ACTION_FAILED;=0A+=09=09=09}=0A+=0A+=09=09=09/*=20Note=20= that=20curl_slist_append()=20makes=20a=20copy=20of=20the=20token.=20*/=0A= +=09=09=09temp=20=3D=20curl_slist_append(*field->target.array,=20token);=0A= +=09=09=09if=20(!temp)=0A+=09=09=09=09return=20JSON_OUT_OF_MEMORY;=0A+=0A= +=09=09=09*field->target.array=20=3D=20temp;=0A+=09=09}=0A+=09}=0A+=09= else=0A+=09{=0A+=09=09/*=20otherwise=20we=20just=20ignore=20it=20*/=0A+=09= }=0A+=0A+=09return=20JSON_SUCCESS;=0A+}=0A+=0A+/*=0A+=20*=20Checks=20the=20= Content-Type=20header=20against=20the=20expected=20type.=20Parameters=20= are=0A+=20*=20allowed=20but=20ignored.=0A+=20*/=0A+static=20bool=0A= +check_content_type(struct=20async_ctx=20*actx,=20const=20char=20*type)=0A= +{=0A+=09const=20size_t=20type_len=20=3D=20strlen(type);=0A+=09char=09=20= =20=20*content_type;=0A+=0A+=09CHECK_GETINFO(actx,=20= CURLINFO_CONTENT_TYPE,=20&content_type,=20return=20false);=0A+=0A+=09if=20= (!content_type)=0A+=09{=0A+=09=09actx_error(actx,=20"no=20content=20type=20= was=20provided");=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09/*=0A+=09=20= *=20We=20need=20to=20perform=20a=20length=20limited=20comparison=20and=20= not=20compare=20the=0A+=09=20*=20whole=20string.=0A+=09=20*/=0A+=09if=20= (pg_strncasecmp(content_type,=20type,=20type_len)=20!=3D=200)=0A+=09=09= goto=20fail;=0A+=0A+=09/*=20On=20an=20exact=20match,=20we're=20done.=20= */=0A+=09Assert(strlen(content_type)=20>=3D=20type_len);=0A+=09if=20= (content_type[type_len]=20=3D=3D=20'\0')=0A+=09=09return=20true;=0A+=0A+=09= /*=0A+=09=20*=20Only=20a=20semicolon=20(optionally=20preceded=20by=20= HTTP=20optional=20whitespace)=20is=0A+=09=20*=20acceptable=20after=20the=20= prefix=20we=20checked.=20This=20marks=20the=20start=20of=20media=0A+=09=20= *=20type=20parameters,=20which=20we=20currently=20have=20no=20use=20for.=0A= +=09=20*/=0A+=09for=20(size_t=20i=20=3D=20type_len;=20content_type[i];=20= ++i)=0A+=09{=0A+=09=09switch=20(content_type[i])=0A+=09=09{=0A+=09=09=09= case=20';':=0A+=09=09=09=09return=20true;=09/*=20success!=20*/=0A+=0A+=09= =09=09case=20'=20':=0A+=09=09=09case=20'\t':=0A+=09=09=09=09/*=20HTTP=20= optional=20whitespace=20allows=20only=20spaces=20and=20htabs.=20*/=0A+=09= =09=09=09break;=0A+=0A+=09=09=09default:=0A+=09=09=09=09goto=20fail;=0A+=09= =09}=0A+=09}=0A+=0A+fail:=0A+=09actx_error(actx,=20"unexpected=20content=20= type:=20\"%s\"",=20content_type);=0A+=09return=20false;=0A+}=0A+=0A+/*=0A= +=20*=20A=20helper=20function=20for=20general=20JSON=20parsing.=20fields=20= is=20the=20array=20of=20field=0A+=20*=20definitions=20with=20their=20= backing=20pointers.=20The=20response=20will=20be=20parsed=20from=0A+=20*=20= actx->curl=20and=20actx->work_data=20(as=20set=20up=20by=20= start_request()),=20and=20any=0A+=20*=20parsing=20errors=20will=20be=20= placed=20into=20actx->errbuf.=0A+=20*/=0A+static=20bool=0A= +parse_oauth_json(struct=20async_ctx=20*actx,=20const=20struct=20= json_field=20*fields)=0A+{=0A+=09PQExpBuffer=20resp=20=3D=20= &actx->work_data;=0A+=09JsonLexContext=20lex=20=3D=20{0};=0A+=09= JsonSemAction=20sem=20=3D=20{0};=0A+=09JsonParseErrorType=20err;=0A+=09= struct=20oauth_parse=20ctx=20=3D=20{0};=0A+=09bool=09=09success=20=3D=20= false;=0A+=0A+=09if=20(!check_content_type(actx,=20"application/json"))=0A= +=09=09return=20false;=0A+=0A+=09if=20(strlen(resp->data)=20!=3D=20= resp->len)=0A+=09{=0A+=09=09actx_error(actx,=20"response=20contains=20= embedded=20NULLs");=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09/*=0A+=09=20= *=20pg_parse_json=20doesn't=20validate=20the=20incoming=20UTF-8,=20so=20= we=20have=20to=20check=0A+=09=20*=20that=20up=20front.=0A+=09=20*/=0A+=09= if=20(pg_encoding_verifymbstr(PG_UTF8,=20resp->data,=20resp->len)=20!=3D=20= resp->len)=0A+=09{=0A+=09=09actx_error(actx,=20"response=20is=20not=20= valid=20UTF-8");=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09= makeJsonLexContextCstringLen(&lex,=20resp->data,=20resp->len,=20PG_UTF8,=20= true);=0A+=09setJsonLexContextOwnsTokens(&lex,=20true);=09/*=20must=20= not=20leak=20on=20error=20*/=0A+=0A+=09ctx.errbuf=20=3D=20&actx->errbuf;=0A= +=09ctx.fields=20=3D=20fields;=0A+=09sem.semstate=20=3D=20&ctx;=0A+=0A+=09= sem.object_start=20=3D=20oauth_json_object_start;=0A+=09= sem.object_field_start=20=3D=20oauth_json_object_field_start;=0A+=09= sem.object_end=20=3D=20oauth_json_object_end;=0A+=09sem.array_start=20=3D=20= oauth_json_array_start;=0A+=09sem.array_end=20=3D=20= oauth_json_array_end;=0A+=09sem.scalar=20=3D=20oauth_json_scalar;=0A+=0A= +=09err=20=3D=20pg_parse_json(&lex,=20&sem);=0A+=0A+=09if=20(err=20!=3D=20= JSON_SUCCESS)=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20For=20= JSON_SEM_ACTION_FAILED,=20we've=20already=20written=20the=20error=0A+=09=09= =20*=20message.=20Other=20errors=20come=20directly=20from=20= pg_parse_json(),=20already=0A+=09=09=20*=20translated.=0A+=09=09=20*/=0A= +=09=09if=20(err=20!=3D=20JSON_SEM_ACTION_FAILED)=0A+=09=09=09= actx_error_str(actx,=20json_errdetail(err,=20&lex));=0A+=0A+=09=09goto=20= cleanup;=0A+=09}=0A+=0A+=09/*=20Check=20all=20required=20fields.=20*/=0A= +=09while=20(fields->name)=0A+=09{=0A+=09=09if=20(fields->required=0A+=09= =09=09&&=20!*fields->target.scalar=0A+=09=09=09&&=20= !*fields->target.array)=0A+=09=09{=0A+=09=09=09actx_error(actx,=20"field=20= \"%s\"=20is=20missing",=20fields->name);=0A+=09=09=09goto=20cleanup;=0A+=09= =09}=0A+=0A+=09=09fields++;=0A+=09}=0A+=0A+=09success=20=3D=20true;=0A+=0A= +cleanup:=0A+=09freeJsonLexContext(&lex);=0A+=09return=20success;=0A+}=0A= +=0A+/*=0A+=20*=20JSON=20Parser=20Definitions=0A+=20*/=0A+=0A+/*=0A+=20*=20= Parses=20authorization=20server=20metadata.=20Fields=20are=20defined=20= by=20OIDC=20Discovery=0A+=20*=201.0=20and=20RFC=208414.=0A+=20*/=0A= +static=20bool=0A+parse_provider(struct=20async_ctx=20*actx,=20struct=20= provider=20*provider)=0A+{=0A+=09struct=20json_field=20fields[]=20=3D=20= {=0A+=09=09{"issuer",=20JSON_TOKEN_STRING,=20{&provider->issuer},=20= REQUIRED},=0A+=09=09{"token_endpoint",=20JSON_TOKEN_STRING,=20= {&provider->token_endpoint},=20REQUIRED},=0A+=0A+=09=09/*----=0A+=09=09=20= *=20The=20following=20fields=20are=20technically=20REQUIRED,=20but=20we=20= don't=20use=0A+=09=09=20*=20them=20anywhere=20yet:=0A+=09=09=20*=0A+=09=09= =20*=20-=20jwks_uri=0A+=09=09=20*=20-=20response_types_supported=0A+=09=09= =20*=20-=20subject_types_supported=0A+=09=09=20*=20-=20= id_token_signing_alg_values_supported=0A+=09=09=20*/=0A+=0A+=09=09= {"device_authorization_endpoint",=20JSON_TOKEN_STRING,=20= {&provider->device_authorization_endpoint},=20OPTIONAL},=0A+=09=09= {"grant_types_supported",=20JSON_TOKEN_ARRAY_START,=20{.array=20=3D=20= &provider->grant_types_supported},=20OPTIONAL},=0A+=0A+=09=09{0},=0A+=09= };=0A+=0A+=09return=20parse_oauth_json(actx,=20fields);=0A+}=0A+=0A+/*=0A= +=20*=20Parses=20a=20valid=20JSON=20number=20into=20a=20double.=20The=20= input=20must=20have=20come=20from=0A+=20*=20pg_parse_json(),=20so=20that=20= we=20know=20the=20lexer=20has=20validated=20it;=20there's=20no=0A+=20*=20= in-band=20signal=20for=20invalid=20formats.=0A+=20*/=0A+static=20double=0A= +parse_json_number(const=20char=20*s)=0A+{=0A+=09double=09=09parsed;=0A+=09= int=09=09=09cnt;=0A+=0A+=09/*=0A+=09=20*=20The=20JSON=20lexer=20has=20= already=20validated=20the=20number,=20which=20is=20stricter=20than=0A+=09= =20*=20the=20%f=20format,=20so=20we=20should=20be=20good=20to=20use=20= sscanf().=0A+=09=20*/=0A+=09cnt=20=3D=20sscanf(s,=20"%lf",=20&parsed);=0A= +=0A+=09if=20(cnt=20!=3D=201)=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20Either=20= the=20lexer=20screwed=20up=20or=20our=20assumption=20above=20isn't=20= true,=20and=0A+=09=09=20*=20either=20way=20a=20developer=20needs=20to=20= take=20a=20look.=0A+=09=09=20*/=0A+=09=09Assert(false);=0A+=09=09return=20= 0;=0A+=09}=0A+=0A+=09return=20parsed;=0A+}=0A+=0A+/*=0A+=20*=20Parses=20= the=20"interval"=20JSON=20number,=20corresponding=20to=20the=20number=20= of=20seconds=20to=0A+=20*=20wait=20between=20token=20endpoint=20= requests.=0A+=20*=0A+=20*=20RFC=208628=20is=20pretty=20silent=20on=20= sanity=20checks=20for=20the=20interval.=20As=20a=20matter=20of=0A+=20*=20= practicality,=20round=20any=20fractional=20intervals=20up=20to=20the=20= next=20second,=20and=20clamp=0A+=20*=20the=20result=20at=20a=20minimum=20= of=20one.=20(Zero-second=20intervals=20would=20result=20in=20an=0A+=20*=20= expensive=20network=20polling=20loop.)=20Tests=20may=20remove=20the=20= lower=20bound=20with=0A+=20*=20PGOAUTHDEBUG,=20for=20improved=20= performance.=0A+=20*/=0A+static=20int=0A+parse_interval(struct=20= async_ctx=20*actx,=20const=20char=20*interval_str)=0A+{=0A+=09double=09=09= parsed;=0A+=0A+=09parsed=20=3D=20parse_json_number(interval_str);=0A+=09= parsed=20=3D=20ceil(parsed);=0A+=0A+=09if=20(parsed=20<=201)=0A+=09=09= return=20actx->debugging=20?=200=20:=201;=0A+=0A+=09else=20if=20(INT_MAX=20= <=3D=20parsed)=0A+=09=09return=20INT_MAX;=0A+=0A+=09return=20parsed;=0A= +}=0A+=0A+/*=0A+=20*=20Parses=20the=20"expires_in"=20JSON=20number,=20= corresponding=20to=20the=20number=20of=20seconds=0A+=20*=20remaining=20= in=20the=20lifetime=20of=20the=20device=20code=20request.=0A+=20*=0A+=20= *=20Similar=20to=20parse_interval,=20but=20we=20have=20even=20fewer=20= requirements=20for=20reasonable=0A+=20*=20values=20since=20we=20don't=20= use=20the=20expiration=20time=20directly=20(it's=20passed=20to=20the=0A+=20= *=20PQAUTHDATA_PROMPT_OAUTH_DEVICE=20hook,=20in=20case=20the=20= application=20wants=20to=20do=0A+=20*=20something=20with=20it).=20We=20= simply=20round=20and=20clamp=20to=20int=20range.=0A+=20*/=0A+static=20= int=0A+parse_expires_in(struct=20async_ctx=20*actx,=20const=20char=20= *expires_in_str)=0A+{=0A+=09double=09=09parsed;=0A+=0A+=09parsed=20=3D=20= parse_json_number(expires_in_str);=0A+=09parsed=20=3D=20round(parsed);=0A= +=0A+=09if=20(INT_MAX=20<=3D=20parsed)=0A+=09=09return=20INT_MAX;=0A+=09= else=20if=20(parsed=20<=3D=20INT_MIN)=0A+=09=09return=20INT_MIN;=0A+=0A+=09= return=20parsed;=0A+}=0A+=0A+/*=0A+=20*=20Parses=20the=20Device=20= Authorization=20Response=20(RFC=208628,=20Sec.=203.2).=0A+=20*/=0A= +static=20bool=0A+parse_device_authz(struct=20async_ctx=20*actx,=20= struct=20device_authz=20*authz)=0A+{=0A+=09struct=20json_field=20= fields[]=20=3D=20{=0A+=09=09{"device_code",=20JSON_TOKEN_STRING,=20= {&authz->device_code},=20REQUIRED},=0A+=09=09{"user_code",=20= JSON_TOKEN_STRING,=20{&authz->user_code},=20REQUIRED},=0A+=09=09= {"verification_uri",=20JSON_TOKEN_STRING,=20{&authz->verification_uri},=20= REQUIRED},=0A+=09=09{"expires_in",=20JSON_TOKEN_NUMBER,=20= {&authz->expires_in_str},=20REQUIRED},=0A+=0A+=09=09/*=0A+=09=09=20*=20= Some=20services=20(Google,=20Azure)=20spell=20verification_uri=20= differently.=0A+=09=09=20*=20We=20accept=20either.=0A+=09=09=20*/=0A+=09=09= {"verification_url",=20JSON_TOKEN_STRING,=20{&authz->verification_uri},=20= REQUIRED},=0A+=0A+=09=09{"verification_uri_complete",=20= JSON_TOKEN_STRING,=20{&authz->verification_uri_complete},=20OPTIONAL},=0A= +=09=09{"interval",=20JSON_TOKEN_NUMBER,=20{&authz->interval_str},=20= OPTIONAL},=0A+=0A+=09=09{0},=0A+=09};=0A+=0A+=09if=20= (!parse_oauth_json(actx,=20fields))=0A+=09=09return=20false;=0A+=0A+=09= /*=0A+=09=20*=20Parse=20our=20numeric=20fields.=20Lexing=20has=20already=20= completed=20by=20this=20time,=20so=0A+=09=20*=20we=20at=20least=20know=20= they're=20valid=20JSON=20numbers.=0A+=09=20*/=0A+=09if=20= (authz->interval_str)=0A+=09=09authz->interval=20=3D=20= parse_interval(actx,=20authz->interval_str);=0A+=09else=0A+=09{=0A+=09=09= /*=0A+=09=09=20*=20RFC=208628=20specifies=205=20seconds=20as=20the=20= default=20value=20if=20the=20server=0A+=09=09=20*=20doesn't=20provide=20= an=20interval.=0A+=09=09=20*/=0A+=09=09authz->interval=20=3D=205;=0A+=09= }=0A+=0A+=09Assert(authz->expires_in_str);=09/*=20ensured=20by=20= parse_oauth_json()=20*/=0A+=09authz->expires_in=20=3D=20= parse_expires_in(actx,=20authz->expires_in_str);=0A+=0A+=09return=20= true;=0A+}=0A+=0A+/*=0A+=20*=20Parses=20the=20device=20access=20token=20= error=20response=20(RFC=208628,=20Sec.=203.5,=20which=0A+=20*=20uses=20= the=20error=20response=20defined=20in=20RFC=206749,=20Sec.=205.2).=0A+=20= */=0A+static=20bool=0A+parse_token_error(struct=20async_ctx=20*actx,=20= struct=20token_error=20*err)=0A+{=0A+=09bool=09=09result;=0A+=09struct=20= json_field=20fields[]=20=3D=20{=0A+=09=09{"error",=20JSON_TOKEN_STRING,=20= {&err->error},=20REQUIRED},=0A+=0A+=09=09{"error_description",=20= JSON_TOKEN_STRING,=20{&err->error_description},=20OPTIONAL},=0A+=0A+=09=09= {0},=0A+=09};=0A+=0A+=09result=20=3D=20parse_oauth_json(actx,=20fields);=0A= +=0A+=09/*=0A+=09=20*=20Since=20token=20errors=20are=20parsed=20during=20= other=20active=20error=20paths,=20only=0A+=09=20*=20override=20the=20= errctx=20if=20parsing=20explicitly=20fails.=0A+=09=20*/=0A+=09if=20= (!result)=0A+=09=09actx->errctx=20=3D=20"failed=20to=20parse=20token=20= error=20response";=0A+=0A+=09return=20result;=0A+}=0A+=0A+/*=0A+=20*=20= Constructs=20a=20message=20from=20the=20token=20error=20response=20and=20= puts=20it=20into=0A+=20*=20actx->errbuf.=0A+=20*/=0A+static=20void=0A= +record_token_error(struct=20async_ctx=20*actx,=20const=20struct=20= token_error=20*err)=0A+{=0A+=09if=20(err->error_description)=0A+=09=09= appendPQExpBuffer(&actx->errbuf,=20"%s=20",=20err->error_description);=0A= +=09else=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20Try=20to=20get=20some=20= more=20helpful=20detail=20into=20the=20error=20string.=20A=20401=0A+=09=09= =20*=20status=20in=20particular=20implies=20that=20the=20= oauth_client_secret=20is=0A+=09=09=20*=20missing=20or=20wrong.=0A+=09=09=20= */=0A+=09=09long=09=09response_code;=0A+=0A+=09=09CHECK_GETINFO(actx,=20= CURLINFO_RESPONSE_CODE,=20&response_code,=20response_code=20=3D=200);=0A= +=0A+=09=09if=20(response_code=20=3D=3D=20401)=0A+=09=09{=0A+=09=09=09= actx_error(actx,=20actx->used_basic_auth=0A+=09=09=09=09=09=20=20=20?=20= "provider=20rejected=20the=20oauth_client_secret"=0A+=09=09=09=09=09=20=20= =20:=20"provider=20requires=20client=20authentication,=20and=20no=20= oauth_client_secret=20is=20set");=0A+=09=09=09actx_error_str(actx,=20"=20= ");=0A+=09=09}=0A+=09}=0A+=0A+=09appendPQExpBuffer(&actx->errbuf,=20= "(%s)",=20err->error);=0A+}=0A+=0A+/*=0A+=20*=20Parses=20the=20device=20= access=20token=20response=20(RFC=208628,=20Sec.=203.5,=20which=20uses=20= the=0A+=20*=20success=20response=20defined=20in=20RFC=206749,=20Sec.=20= 5.1).=0A+=20*/=0A+static=20bool=0A+parse_access_token(struct=20async_ctx=20= *actx,=20struct=20token=20*tok)=0A+{=0A+=09struct=20json_field=20= fields[]=20=3D=20{=0A+=09=09{"access_token",=20JSON_TOKEN_STRING,=20= {&tok->access_token},=20REQUIRED},=0A+=09=09{"token_type",=20= JSON_TOKEN_STRING,=20{&tok->token_type},=20REQUIRED},=0A+=0A+=09=09/*---=0A= +=09=09=20*=20We=20currently=20have=20no=20use=20for=20the=20following=20= OPTIONAL=20fields:=0A+=09=09=20*=0A+=09=09=20*=20-=20expires_in:=20This=20= will=20be=20important=20for=20maintaining=20a=20token=20cache,=0A+=09=09=20= *=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20but=20we=20do=20not=20yet=20= implement=20one.=0A+=09=09=20*=0A+=09=09=20*=20-=20refresh_token:=20= Ditto.=0A+=09=09=20*=0A+=09=09=20*=20-=20scope:=20This=20is=20only=20= sent=20when=20the=20authorization=20server=20sees=20fit=20to=0A+=09=09=20= *=20=20=20=20=20=20=20=20=20=20change=20our=20scope=20request.=20It's=20= not=20clear=20what=20we=20should=20do=0A+=09=09=20*=20=20=20=20=20=20=20=20= =20=20about=20this;=20either=20it's=20been=20done=20as=20a=20matter=20of=20= policy,=20or=0A+=09=09=20*=20=20=20=20=20=20=20=20=20=20the=20user=20has=20= explicitly=20denied=20part=20of=20the=20authorization,=0A+=09=09=20*=20=20= =20=20=20=20=20=20=20=20and=20either=20way=20the=20server-side=20= validator=20is=20in=20a=20better=0A+=09=09=20*=20=20=20=20=20=20=20=20=20= =20place=20to=20complain=20if=20the=20change=20isn't=20acceptable.=0A+=09= =09=20*/=0A+=0A+=09=09{0},=0A+=09};=0A+=0A+=09return=20= parse_oauth_json(actx,=20fields);=0A+}=0A+=0A+/*=0A+=20*=20libcurl=20= Multi=20Setup/Callbacks=0A+=20*/=0A+=0A+/*=0A+=20*=20Sets=20up=20the=20= actx->mux,=20which=20is=20the=20altsock=20that=20PQconnectPoll=20clients=20= will=0A+=20*=20select()=20on=20instead=20of=20the=20Postgres=20socket=20= during=20OAuth=20negotiation.=0A+=20*=0A+=20*=20This=20is=20just=20an=20= epoll=20set=20or=20kqueue=20abstracting=20multiple=20other=20= descriptors.=0A+=20*=20For=20epoll,=20the=20timerfd=20is=20always=20part=20= of=20the=20set;=20it's=20just=20disabled=20when=0A+=20*=20we're=20not=20= using=20it.=20For=20kqueue,=20the=20"timerfd"=20is=20actually=20a=20= second=20kqueue=0A+=20*=20instance=20which=20is=20only=20added=20to=20= the=20set=20when=20needed.=0A+=20*/=0A+static=20bool=0A= +setup_multiplexer(struct=20async_ctx=20*actx)=0A+{=0A+#ifdef=20= HAVE_SYS_EPOLL_H=0A+=09struct=20epoll_event=20ev=20=3D=20{.events=20=3D=20= EPOLLIN};=0A+=0A+=09actx->mux=20=3D=20epoll_create1(EPOLL_CLOEXEC);=0A+=09= if=20(actx->mux=20<=200)=0A+=09{=0A+=09=09actx_error(actx,=20"failed=20= to=20create=20epoll=20set:=20%m");=0A+=09=09return=20false;=0A+=09}=0A+=0A= +=09actx->timerfd=20=3D=20timerfd_create(CLOCK_MONOTONIC,=20= TFD_CLOEXEC);=0A+=09if=20(actx->timerfd=20<=200)=0A+=09{=0A+=09=09= actx_error(actx,=20"failed=20to=20create=20timerfd:=20%m");=0A+=09=09= return=20false;=0A+=09}=0A+=0A+=09if=20(epoll_ctl(actx->mux,=20= EPOLL_CTL_ADD,=20actx->timerfd,=20&ev)=20<=200)=0A+=09{=0A+=09=09= actx_error(actx,=20"failed=20to=20add=20timerfd=20to=20epoll=20set:=20= %m");=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09return=20true;=0A= +#endif=0A+#ifdef=20HAVE_SYS_EVENT_H=0A+=09actx->mux=20=3D=20kqueue();=0A= +=09if=20(actx->mux=20<=200)=0A+=09{=0A+=09=09/*-=20translator:=20the=20= term=20"kqueue"=20(kernel=20queue)=20should=20not=20be=20translated=20*/=0A= +=09=09actx_error(actx,=20"failed=20to=20create=20kqueue:=20%m");=0A+=09=09= return=20false;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20Originally,=20we=20set=20= EVFILT_TIMER=20directly=20on=20the=20top-level=20multiplexer.=0A+=09=20*=20= This=20makes=20it=20difficult=20to=20implement=20timer_expired(),=20= though,=20so=20now=20we=0A+=09=20*=20set=20EVFILT_TIMER=20on=20a=20= separate=20actx->timerfd,=20which=20is=20chained=20to=0A+=09=20*=20= actx->mux=20while=20the=20timer=20is=20active.=0A+=09=20*/=0A+=09= actx->timerfd=20=3D=20kqueue();=0A+=09if=20(actx->timerfd=20<=200)=0A+=09= {=0A+=09=09actx_error(actx,=20"failed=20to=20create=20timer=20kqueue:=20= %m");=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09return=20true;=0A= +#endif=0A+=0A+=09actx_error(actx,=20"libpq=20does=20not=20support=20the=20= Device=20Authorization=20flow=20on=20this=20platform");=0A+=09return=20= false;=0A+}=0A+=0A+/*=0A+=20*=20Adds=20and=20removes=20sockets=20from=20= the=20multiplexer=20set,=20as=20directed=20by=20the=0A+=20*=20libcurl=20= multi=20handle.=0A+=20*/=0A+static=20int=0A+register_socket(CURL=20= *curl,=20curl_socket_t=20socket,=20int=20what,=20void=20*ctx,=0A+=09=09=09= =09void=20*socketp)=0A+{=0A+#ifdef=20HAVE_SYS_EPOLL_H=0A+=09struct=20= async_ctx=20*actx=20=3D=20ctx;=0A+=09struct=20epoll_event=20ev=20=3D=20= {0};=0A+=09int=09=09=09res;=0A+=09int=09=09=09op=20=3D=20EPOLL_CTL_ADD;=0A= +=0A+=09switch=20(what)=0A+=09{=0A+=09=09case=20CURL_POLL_IN:=0A+=09=09=09= ev.events=20=3D=20EPOLLIN;=0A+=09=09=09break;=0A+=0A+=09=09case=20= CURL_POLL_OUT:=0A+=09=09=09ev.events=20=3D=20EPOLLOUT;=0A+=09=09=09= break;=0A+=0A+=09=09case=20CURL_POLL_INOUT:=0A+=09=09=09ev.events=20=3D=20= EPOLLIN=20|=20EPOLLOUT;=0A+=09=09=09break;=0A+=0A+=09=09case=20= CURL_POLL_REMOVE:=0A+=09=09=09op=20=3D=20EPOLL_CTL_DEL;=0A+=09=09=09= break;=0A+=0A+=09=09default:=0A+=09=09=09actx_error(actx,=20"unknown=20= libcurl=20socket=20operation:=20%d",=20what);=0A+=09=09=09return=20-1;=0A= +=09}=0A+=0A+=09res=20=3D=20epoll_ctl(actx->mux,=20op,=20socket,=20&ev);=0A= +=09if=20(res=20<=200=20&&=20errno=20=3D=3D=20EEXIST)=0A+=09{=0A+=09=09= /*=20We=20already=20had=20this=20socket=20in=20the=20pollset.=20*/=0A+=09= =09op=20=3D=20EPOLL_CTL_MOD;=0A+=09=09res=20=3D=20epoll_ctl(actx->mux,=20= op,=20socket,=20&ev);=0A+=09}=0A+=0A+=09if=20(res=20<=200)=0A+=09{=0A+=09= =09switch=20(op)=0A+=09=09{=0A+=09=09=09case=20EPOLL_CTL_ADD:=0A+=09=09=09= =09actx_error(actx,=20"could=20not=20add=20to=20epoll=20set:=20%m");=0A+=09= =09=09=09break;=0A+=0A+=09=09=09case=20EPOLL_CTL_DEL:=0A+=09=09=09=09= actx_error(actx,=20"could=20not=20delete=20from=20epoll=20set:=20%m");=0A= +=09=09=09=09break;=0A+=0A+=09=09=09default:=0A+=09=09=09=09= actx_error(actx,=20"could=20not=20update=20epoll=20set:=20%m");=0A+=09=09= }=0A+=0A+=09=09return=20-1;=0A+=09}=0A+#endif=0A+#ifdef=20= HAVE_SYS_EVENT_H=0A+=09struct=20async_ctx=20*actx=20=3D=20ctx;=0A+=09= struct=20kevent=20ev[2]=20=3D=20{{0}};=0A+=09struct=20kevent=20= ev_out[2];=0A+=09struct=20timespec=20timeout=20=3D=20{0};=0A+=09int=09=09= =09nev=20=3D=200;=0A+=09int=09=09=09res;=0A+=0A+=09switch=20(what)=0A+=09= {=0A+=09=09case=20CURL_POLL_IN:=0A+=09=09=09EV_SET(&ev[nev],=20socket,=20= EVFILT_READ,=20EV_ADD=20|=20EV_RECEIPT,=200,=200,=200);=0A+=09=09=09= nev++;=0A+=09=09=09break;=0A+=0A+=09=09case=20CURL_POLL_OUT:=0A+=09=09=09= EV_SET(&ev[nev],=20socket,=20EVFILT_WRITE,=20EV_ADD=20|=20EV_RECEIPT,=20= 0,=200,=200);=0A+=09=09=09nev++;=0A+=09=09=09break;=0A+=0A+=09=09case=20= CURL_POLL_INOUT:=0A+=09=09=09EV_SET(&ev[nev],=20socket,=20EVFILT_READ,=20= EV_ADD=20|=20EV_RECEIPT,=200,=200,=200);=0A+=09=09=09nev++;=0A+=09=09=09= EV_SET(&ev[nev],=20socket,=20EVFILT_WRITE,=20EV_ADD=20|=20EV_RECEIPT,=20= 0,=200,=200);=0A+=09=09=09nev++;=0A+=09=09=09break;=0A+=0A+=09=09case=20= CURL_POLL_REMOVE:=0A+=0A+=09=09=09/*=0A+=09=09=09=20*=20We=20don't=20= know=20which=20of=20these=20is=20currently=20registered,=20perhaps=0A+=09= =09=09=20*=20both,=20so=20we=20try=20to=20remove=20both.=20=20This=20= means=20we=20need=20to=20tolerate=0A+=09=09=09=20*=20ENOENT=20below.=0A+=09= =09=09=20*/=0A+=09=09=09EV_SET(&ev[nev],=20socket,=20EVFILT_READ,=20= EV_DELETE=20|=20EV_RECEIPT,=200,=200,=200);=0A+=09=09=09nev++;=0A+=09=09=09= EV_SET(&ev[nev],=20socket,=20EVFILT_WRITE,=20EV_DELETE=20|=20EV_RECEIPT,=20= 0,=200,=200);=0A+=09=09=09nev++;=0A+=09=09=09break;=0A+=0A+=09=09= default:=0A+=09=09=09actx_error(actx,=20"unknown=20libcurl=20socket=20= operation:=20%d",=20what);=0A+=09=09=09return=20-1;=0A+=09}=0A+=0A+=09= res=20=3D=20kevent(actx->mux,=20ev,=20nev,=20ev_out,=20lengthof(ev_out),=20= &timeout);=0A+=09if=20(res=20<=200)=0A+=09{=0A+=09=09actx_error(actx,=20= "could=20not=20modify=20kqueue:=20%m");=0A+=09=09return=20-1;=0A+=09}=0A= +=0A+=09/*=0A+=09=20*=20We=20can't=20use=20the=20simple=20errno=20= version=20of=20kevent,=20because=20we=20need=20to=0A+=09=20*=20skip=20= over=20ENOENT=20while=20still=20allowing=20a=20second=20change=20to=20be=20= processed.=0A+=09=20*=20So=20we=20need=20a=20longer-form=20error=20= checking=20loop.=0A+=09=20*/=0A+=09for=20(int=20i=20=3D=200;=20i=20<=20= res;=20++i)=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20EV_RECEIPT=20should=20= guarantee=20one=20EV_ERROR=20result=20for=20every=20change,=0A+=09=09=20= *=20whether=20successful=20or=20not.=20Failed=20entries=20contain=20a=20= non-zero=20errno=0A+=09=09=20*=20in=20the=20data=20field.=0A+=09=09=20*/=0A= +=09=09Assert(ev_out[i].flags=20&=20EV_ERROR);=0A+=0A+=09=09errno=20=3D=20= ev_out[i].data;=0A+=09=09if=20(errno=20&&=20errno=20!=3D=20ENOENT)=0A+=09= =09{=0A+=09=09=09switch=20(what)=0A+=09=09=09{=0A+=09=09=09=09case=20= CURL_POLL_REMOVE:=0A+=09=09=09=09=09actx_error(actx,=20"could=20not=20= delete=20from=20kqueue:=20%m");=0A+=09=09=09=09=09break;=0A+=09=09=09=09= default:=0A+=09=09=09=09=09actx_error(actx,=20"could=20not=20add=20to=20= kqueue:=20%m");=0A+=09=09=09}=0A+=09=09=09return=20-1;=0A+=09=09}=0A+=09= }=0A+#endif=0A+=0A+=09return=200;=0A+}=0A+=0A+/*=0A+=20*=20Enables=20or=20= disables=20the=20timer=20in=20the=20multiplexer=20set.=20The=20timeout=20= value=20is=0A+=20*=20in=20milliseconds=20(negative=20values=20disable=20= the=20timer).=0A+=20*=0A+=20*=20For=20epoll,=20rather=20than=20= continually=20adding=20and=20removing=20the=20timer,=20we=20keep=20it=0A= +=20*=20in=20the=20set=20at=20all=20times=20and=20just=20disarm=20it=20= when=20it's=20not=20needed.=20For=20kqueue,=0A+=20*=20the=20timer=20is=20= removed=20completely=20when=20disabled=20to=20prevent=20stale=20timeouts=20= from=0A+=20*=20remaining=20in=20the=20queue.=0A+=20*/=0A+static=20bool=0A= +set_timer(struct=20async_ctx=20*actx,=20long=20timeout)=0A+{=0A+#if=20= HAVE_SYS_EPOLL_H=0A+=09struct=20itimerspec=20spec=20=3D=20{0};=0A+=0A+=09= if=20(timeout=20<=200)=0A+=09{=0A+=09=09/*=20the=20zero=20itimerspec=20= will=20disarm=20the=20timer=20below=20*/=0A+=09}=0A+=09else=20if=20= (timeout=20=3D=3D=200)=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20A=20zero=20= timeout=20means=20libcurl=20wants=20us=20to=20call=20back=20immediately.=0A= +=09=09=20*=20That's=20not=20technically=20an=20option=20for=20timerfd,=20= but=20we=20can=20make=20the=0A+=09=09=20*=20timeout=20ridiculously=20= short.=0A+=09=09=20*/=0A+=09=09spec.it_value.tv_nsec=20=3D=201;=0A+=09}=0A= +=09else=0A+=09{=0A+=09=09spec.it_value.tv_sec=20=3D=20timeout=20/=20= 1000;=0A+=09=09spec.it_value.tv_nsec=20=3D=20(timeout=20%=201000)=20*=20= 1000000;=0A+=09}=0A+=0A+=09if=20(timerfd_settime(actx->timerfd,=200=20/*=20= no=20flags=20*/=20,=20&spec,=20NULL)=20<=200)=0A+=09{=0A+=09=09= actx_error(actx,=20"setting=20timerfd=20to=20%ld:=20%m",=20timeout);=0A+=09= =09return=20false;=0A+=09}=0A+=0A+=09return=20true;=0A+#endif=0A+#ifdef=20= HAVE_SYS_EVENT_H=0A+=09struct=20kevent=20ev;=0A+=0A+=09/*=20= Enable/disable=20the=20timer=20itself.=20*/=0A+=09EV_SET(&ev,=201,=20= EVFILT_TIMER,=20timeout=20<=200=20?=20EV_DELETE=20:=20(EV_ADD=20|=20= EV_ONESHOT),=0A+=09=09=20=20=200,=20timeout,=200);=0A+=09if=20= (kevent(actx->timerfd,=20&ev,=201,=20NULL,=200,=20NULL)=20<=200=20&&=20= errno=20!=3D=20ENOENT)=0A+=09{=0A+=09=09actx_error(actx,=20"setting=20= kqueue=20timer=20to=20%ld:=20%m",=20timeout);=0A+=09=09return=20false;=0A= +=09}=0A+=0A+=09/*=0A+=09=20*=20Add/remove=20the=20timer=20to/from=20the=20= mux.=20(In=20contrast=20with=20epoll,=20if=20we=0A+=09=20*=20allowed=20= the=20timer=20to=20remain=20registered=20here=20after=20being=20= disabled,=20the=0A+=09=20*=20mux=20queue=20would=20retain=20any=20= previous=20stale=20timeout=20notifications=20and=0A+=09=20*=20remain=20= readable.)=0A+=09=20*/=0A+=09EV_SET(&ev,=20actx->timerfd,=20EVFILT_READ,=20= timeout=20<=200=20?=20EV_DELETE=20:=20EV_ADD,=0A+=09=09=20=20=200,=200,=20= 0);=0A+=09if=20(kevent(actx->mux,=20&ev,=201,=20NULL,=200,=20NULL)=20<=20= 0=20&&=20errno=20!=3D=20ENOENT)=0A+=09{=0A+=09=09actx_error(actx,=20= "could=20not=20update=20timer=20on=20kqueue:=20%m");=0A+=09=09return=20= false;=0A+=09}=0A+=0A+=09return=20true;=0A+#endif=0A+=0A+=09= actx_error(actx,=20"libpq=20does=20not=20support=20timers=20on=20this=20= platform");=0A+=09return=20false;=0A+}=0A+=0A+/*=0A+=20*=20Returns=201=20= if=20the=20timeout=20in=20the=20multiplexer=20set=20has=20expired=20= since=20the=20last=0A+=20*=20call=20to=20set_timer(),=200=20if=20the=20= timer=20is=20still=20running,=20or=20-1=20(with=20an=0A+=20*=20= actx_error()=20report)=20if=20the=20timer=20cannot=20be=20queried.=0A+=20= */=0A+static=20int=0A+timer_expired(struct=20async_ctx=20*actx)=0A+{=0A= +#if=20HAVE_SYS_EPOLL_H=0A+=09struct=20itimerspec=20spec=20=3D=20{0};=0A= +=0A+=09if=20(timerfd_gettime(actx->timerfd,=20&spec)=20<=200)=0A+=09{=0A= +=09=09actx_error(actx,=20"getting=20timerfd=20value:=20%m");=0A+=09=09= return=20-1;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20This=20implementation=20= assumes=20we're=20using=20single-shot=20timers.=20If=20you=0A+=09=20*=20= change=20to=20using=20intervals,=20you'll=20need=20to=20reimplement=20= this=20function=0A+=09=20*=20too,=20possibly=20with=20the=20read()=20or=20= select()=20interfaces=20for=20timerfd.=0A+=09=20*/=0A+=09= Assert(spec.it_interval.tv_sec=20=3D=3D=200=0A+=09=09=20=20=20&&=20= spec.it_interval.tv_nsec=20=3D=3D=200);=0A+=0A+=09/*=20If=20the=20= remaining=20time=20to=20expiration=20is=20zero,=20we're=20done.=20*/=0A+=09= return=20(spec.it_value.tv_sec=20=3D=3D=200=0A+=09=09=09&&=20= spec.it_value.tv_nsec=20=3D=3D=200);=0A+#endif=0A+#ifdef=20= HAVE_SYS_EVENT_H=0A+=09int=09=09=09res;=0A+=0A+=09/*=20Is=20the=20timer=20= queue=20ready?=20*/=0A+=09res=20=3D=20PQsocketPoll(actx->timerfd,=201=20= /*=20forRead=20*/=20,=200,=200);=0A+=09if=20(res=20<=200)=0A+=09{=0A+=09=09= actx_error(actx,=20"checking=20kqueue=20for=20timeout:=20%m");=0A+=09=09= return=20-1;=0A+=09}=0A+=0A+=09return=20(res=20>=200);=0A+#endif=0A+=0A+=09= actx_error(actx,=20"libpq=20does=20not=20support=20timers=20on=20this=20= platform");=0A+=09return=20-1;=0A+}=0A+=0A+/*=0A+=20*=20Adds=20or=20= removes=20timeouts=20from=20the=20multiplexer=20set,=20as=20directed=20= by=20the=0A+=20*=20libcurl=20multi=20handle.=0A+=20*/=0A+static=20int=0A= +register_timer(CURLM=20*curlm,=20long=20timeout,=20void=20*ctx)=0A+{=0A= +=09struct=20async_ctx=20*actx=20=3D=20ctx;=0A+=0A+=09/*=0A+=09=20*=20= There=20might=20be=20an=20optimization=20opportunity=20here:=20if=20= timeout=20=3D=3D=200,=20we=0A+=09=20*=20could=20signal=20drive_request=20= to=20immediately=20call=0A+=09=20*=20curl_multi_socket_action,=20rather=20= than=20returning=20all=20the=20way=20up=20the=0A+=09=20*=20stack=20only=20= to=20come=20right=20back.=20But=20it's=20not=20clear=20that=20the=20= additional=0A+=09=20*=20code=20complexity=20is=20worth=20it.=0A+=09=20*/=0A= +=09if=20(!set_timer(actx,=20timeout))=0A+=09=09return=20-1;=09=09=09=09= /*=20actx_error=20already=20called=20*/=0A+=0A+=09return=200;=0A+}=0A+=0A= +/*=0A+=20*=20Prints=20Curl=20request=20debugging=20information=20to=20= stderr.=0A+=20*=0A+=20*=20Note=20that=20this=20will=20expose=20a=20= number=20of=20critical=20secrets,=20so=20users=20have=20to=20opt=0A+=20*=20= into=20this=20(see=20PGOAUTHDEBUG).=0A+=20*/=0A+static=20int=0A= +debug_callback(CURL=20*handle,=20curl_infotype=20type,=20char=20*data,=20= size_t=20size,=0A+=09=09=09=20=20=20void=20*clientp)=0A+{=0A+=09const=20= char=20*prefix;=0A+=09bool=09=09printed_prefix=20=3D=20false;=0A+=09= PQExpBufferData=20buf;=0A+=0A+=09/*=20Prefixes=20are=20modeled=20off=20= of=20the=20default=20libcurl=20debug=20output.=20*/=0A+=09switch=20= (type)=0A+=09{=0A+=09=09case=20CURLINFO_TEXT:=0A+=09=09=09prefix=20=3D=20= "*";=0A+=09=09=09break;=0A+=0A+=09=09case=20CURLINFO_HEADER_IN:=09/*=20= fall=20through=20*/=0A+=09=09case=20CURLINFO_DATA_IN:=0A+=09=09=09prefix=20= =3D=20"<";=0A+=09=09=09break;=0A+=0A+=09=09case=20CURLINFO_HEADER_OUT:=09= /*=20fall=20through=20*/=0A+=09=09case=20CURLINFO_DATA_OUT:=0A+=09=09=09= prefix=20=3D=20">";=0A+=09=09=09break;=0A+=0A+=09=09default:=0A+=09=09=09= return=200;=0A+=09}=0A+=0A+=09initPQExpBuffer(&buf);=0A+=0A+=09/*=0A+=09=20= *=20Split=20the=20output=20into=20lines=20for=20readability;=20sometimes=20= multiple=20headers=0A+=09=20*=20are=20included=20in=20a=20single=20call.=20= We=20also=20don't=20allow=20unprintable=20ASCII=0A+=09=20*=20through=20= without=20a=20basic=20=20escape.=0A+=09=20*/=0A+=09for=20(int=20i=20= =3D=200;=20i=20<=20size;=20i++)=0A+=09{=0A+=09=09char=09=09c=20=3D=20= data[i];=0A+=0A+=09=09if=20(!printed_prefix)=0A+=09=09{=0A+=09=09=09= appendPQExpBuffer(&buf,=20"[libcurl]=20%s=20",=20prefix);=0A+=09=09=09= printed_prefix=20=3D=20true;=0A+=09=09}=0A+=0A+=09=09if=20(c=20>=3D=20= 0x20=20&&=20c=20<=3D=200x7E)=0A+=09=09=09appendPQExpBufferChar(&buf,=20= c);=0A+=09=09else=20if=20((type=20=3D=3D=20CURLINFO_HEADER_IN=0A+=09=09=09= =09=20=20||=20type=20=3D=3D=20CURLINFO_HEADER_OUT=0A+=09=09=09=09=20=20= ||=20type=20=3D=3D=20CURLINFO_TEXT)=0A+=09=09=09=09=20&&=20(c=20=3D=3D=20= '\r'=20||=20c=20=3D=3D=20'\n'))=0A+=09=09{=0A+=09=09=09/*=0A+=09=09=09=20= *=20Don't=20bother=20emitting=20<0D><0A>=20for=20headers=20and=20text;=20= it's=20not=0A+=09=09=09=20*=20helpful=20noise.=0A+=09=09=09=20*/=0A+=09=09= }=0A+=09=09else=0A+=09=09=09appendPQExpBuffer(&buf,=20"<%02X>",=20c);=0A= +=0A+=09=09if=20(c=20=3D=3D=20'\n')=0A+=09=09{=0A+=09=09=09= appendPQExpBufferChar(&buf,=20c);=0A+=09=09=09printed_prefix=20=3D=20= false;=0A+=09=09}=0A+=09}=0A+=0A+=09if=20(printed_prefix)=0A+=09=09= appendPQExpBufferChar(&buf,=20'\n');=09/*=20finish=20the=20line=20*/=0A+=0A= +=09fprintf(stderr,=20"%s",=20buf.data);=0A+=09termPQExpBuffer(&buf);=0A= +=09return=200;=0A+}=0A+=0A+/*=0A+=20*=20Initializes=20the=20two=20= libcurl=20handles=20in=20the=20async_ctx.=20The=20multi=20handle,=0A+=20= *=20actx->curlm,=20is=20what=20drives=20the=20asynchronous=20engine=20= and=20tells=20us=20what=20to=20do=0A+=20*=20next.=20The=20easy=20handle,=20= actx->curl,=20encapsulates=20the=20state=20for=20a=20single=0A+=20*=20= request/response.=20It's=20added=20to=20the=20multi=20handle=20as=20= needed,=20during=0A+=20*=20start_request().=0A+=20*/=0A+static=20bool=0A= +setup_curl_handles(struct=20async_ctx=20*actx)=0A+{=0A+=09/*=0A+=09=20*=20= Create=20our=20multi=20handle.=20This=20encapsulates=20the=20entire=20= conversation=20with=0A+=09=20*=20libcurl=20for=20this=20connection.=0A+=09= =20*/=0A+=09actx->curlm=20=3D=20curl_multi_init();=0A+=09if=20= (!actx->curlm)=0A+=09{=0A+=09=09/*=20We=20don't=20get=20a=20lot=20of=20= feedback=20on=20the=20failure=20reason.=20*/=0A+=09=09actx_error(actx,=20= "failed=20to=20create=20libcurl=20multi=20handle");=0A+=09=09return=20= false;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20The=20multi=20handle=20tells=20= us=20what=20to=20wait=20on=20using=20two=20callbacks.=20These=0A+=09=20*=20= will=20manipulate=20actx->mux=20as=20needed.=0A+=09=20*/=0A+=09= CHECK_MSETOPT(actx,=20CURLMOPT_SOCKETFUNCTION,=20register_socket,=20= return=20false);=0A+=09CHECK_MSETOPT(actx,=20CURLMOPT_SOCKETDATA,=20= actx,=20return=20false);=0A+=09CHECK_MSETOPT(actx,=20= CURLMOPT_TIMERFUNCTION,=20register_timer,=20return=20false);=0A+=09= CHECK_MSETOPT(actx,=20CURLMOPT_TIMERDATA,=20actx,=20return=20false);=0A+=0A= +=09/*=0A+=09=20*=20Set=20up=20an=20easy=20handle.=20All=20of=20our=20= requests=20are=20made=20serially,=20so=20we=0A+=09=20*=20only=20ever=20= need=20to=20keep=20track=20of=20one.=0A+=09=20*/=0A+=09actx->curl=20=3D=20= curl_easy_init();=0A+=09if=20(!actx->curl)=0A+=09{=0A+=09=09= actx_error(actx,=20"failed=20to=20create=20libcurl=20handle");=0A+=09=09= return=20false;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20Multi-threaded=20= applications=20must=20set=20CURLOPT_NOSIGNAL.=20This=20requires=20us=0A+=09= =20*=20to=20handle=20the=20possibility=20of=20SIGPIPE=20ourselves=20= using=20pq_block_sigpipe;=0A+=09=20*=20see=20pg_fe_run_oauth_flow().=0A+=09= =20*=0A+=09=20*=20NB:=20If=20libcurl=20is=20not=20built=20against=20a=20= friendly=20DNS=20resolver=20(c-ares=20or=0A+=09=20*=20threaded),=20= setting=20this=20option=20prevents=20DNS=20lookups=20from=20timing=20out=0A= +=09=20*=20correctly.=20We=20warn=20about=20this=20situation=20at=20= configure=20time.=0A+=09=20*=0A+=09=20*=20TODO:=20Perhaps=20there's=20a=20= clever=20way=20to=20warn=20the=20user=20about=20synchronous=0A+=09=20*=20= DNS=20at=20runtime=20too?=20It's=20not=20immediately=20clear=20how=20to=20= do=20that=20in=20a=0A+=09=20*=20helpful=20way:=20for=20many=20standard=20= single-threaded=20use=20cases,=20the=20user=0A+=09=20*=20might=20not=20= care=20at=20all,=20so=20spraying=20warnings=20to=20stderr=20would=20= probably=20do=0A+=09=20*=20more=20harm=20than=20good.=0A+=09=20*/=0A+=09= CHECK_SETOPT(actx,=20CURLOPT_NOSIGNAL,=201L,=20return=20false);=0A+=0A+=09= if=20(actx->debugging)=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20Set=20a=20= callback=20for=20retrieving=20error=20information=20from=20libcurl,=20= the=0A+=09=09=20*=20function=20only=20takes=20effect=20when=20= CURLOPT_VERBOSE=20has=20been=20set=20so=0A+=09=09=20*=20make=20sure=20= the=20order=20is=20kept.=0A+=09=09=20*/=0A+=09=09CHECK_SETOPT(actx,=20= CURLOPT_DEBUGFUNCTION,=20debug_callback,=20return=20false);=0A+=09=09= CHECK_SETOPT(actx,=20CURLOPT_VERBOSE,=201L,=20return=20false);=0A+=09}=0A= +=0A+=09CHECK_SETOPT(actx,=20CURLOPT_ERRORBUFFER,=20actx->curl_err,=20= return=20false);=0A+=0A+=09/*=0A+=09=20*=20Only=20HTTPS=20is=20allowed.=20= (Debug=20mode=20additionally=20allows=20HTTP;=20this=20is=0A+=09=20*=20= intended=20for=20testing=20only.)=0A+=09=20*=0A+=09=20*=20There's=20a=20= bit=20of=20unfortunate=20complexity=20around=20the=20choice=20of=0A+=09=20= *=20CURLoption.=20CURLOPT_PROTOCOLS=20is=20deprecated=20in=20modern=20= Curls,=20but=20its=0A+=09=20*=20replacement=20didn't=20show=20up=20until=20= relatively=20recently.=0A+=09=20*/=0A+=09{=0A+#if=20= CURL_AT_LEAST_VERSION(7,=2085,=200)=0A+=09=09const=20CURLoption=20popt=20= =3D=20CURLOPT_PROTOCOLS_STR;=0A+=09=09const=20char=20*protos=20=3D=20= "https";=0A+=09=09const=20char=20*const=20unsafe=20=3D=20"https,http";=0A= +#else=0A+=09=09const=20CURLoption=20popt=20=3D=20CURLOPT_PROTOCOLS;=0A+=09= =09long=09=09protos=20=3D=20CURLPROTO_HTTPS;=0A+=09=09const=20long=09= unsafe=20=3D=20CURLPROTO_HTTPS=20|=20CURLPROTO_HTTP;=0A+#endif=0A+=0A+=09= =09if=20(actx->debugging)=0A+=09=09=09protos=20=3D=20unsafe;=0A+=0A+=09=09= CHECK_SETOPT(actx,=20popt,=20protos,=20return=20false);=0A+=09}=0A+=0A+=09= /*=0A+=09=20*=20If=20we're=20in=20debug=20mode,=20allow=20the=20= developer=20to=20change=20the=20trusted=20CA=0A+=09=20*=20list.=20For=20= now,=20this=20is=20not=20something=20we=20expose=20outside=20of=20the=20= UNSAFE=0A+=09=20*=20mode,=20because=20it's=20not=20clear=20that=20it's=20= useful=20in=20production:=20both=20libpq=0A+=09=20*=20and=20the=20user's=20= browser=20must=20trust=20the=20same=20authorization=20servers=20for=0A+=09= =20*=20the=20flow=20to=20work=20at=20all,=20so=20any=20changes=20to=20= the=20roots=20are=20likely=20to=20be=0A+=09=20*=20done=20system-wide.=0A= +=09=20*/=0A+=09if=20(actx->debugging)=0A+=09{=0A+=09=09const=20char=20= *env;=0A+=0A+=09=09if=20((env=20=3D=20getenv("PGOAUTHCAFILE"))=20!=3D=20= NULL)=0A+=09=09=09CHECK_SETOPT(actx,=20CURLOPT_CAINFO,=20env,=20return=20= false);=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20Suppress=20the=20Accept=20= header=20to=20make=20our=20request=20as=20minimal=20as=20possible.=0A+=09= =20*=20(Ideally=20we=20would=20set=20it=20to=20"application/json"=20= instead,=20but=20OpenID=20is=0A+=09=20*=20pretty=20strict=20when=20it=20= comes=20to=20provider=20behavior,=20so=20we=20have=20to=20check=0A+=09=20= *=20what=20comes=20back=20anyway.)=0A+=09=20*/=0A+=09actx->headers=20=3D=20= curl_slist_append(actx->headers,=20"Accept:");=0A+=09if=20(actx->headers=20= =3D=3D=20NULL)=0A+=09{=0A+=09=09actx_error(actx,=20"out=20of=20memory");=0A= +=09=09return=20false;=0A+=09}=0A+=09CHECK_SETOPT(actx,=20= CURLOPT_HTTPHEADER,=20actx->headers,=20return=20false);=0A+=0A+=09return=20= true;=0A+}=0A+=0A+/*=0A+=20*=20Generic=20HTTP=20Request=20Handlers=0A+=20= */=0A+=0A+/*=0A+=20*=20Response=20callback=20from=20libcurl=20which=20= appends=20the=20response=20body=20into=0A+=20*=20actx->work_data=20(see=20= start_request()).=20The=20maximum=20size=20of=20the=20data=20is=0A+=20*=20= defined=20by=20CURL_MAX_WRITE_SIZE=20which=20by=20default=20is=2016kb=20= (and=20can=20only=20be=0A+=20*=20changed=20by=20recompiling=20libcurl).=0A= +=20*/=0A+static=20size_t=0A+append_data(char=20*buf,=20size_t=20size,=20= size_t=20nmemb,=20void=20*userdata)=0A+{=0A+=09struct=20async_ctx=20= *actx=20=3D=20userdata;=0A+=09PQExpBuffer=20resp=20=3D=20= &actx->work_data;=0A+=09size_t=09=09len=20=3D=20size=20*=20nmemb;=0A+=0A= +=09/*=20In=20case=20we=20receive=20data=20over=20the=20threshold,=20= abort=20the=20transfer=20*/=0A+=09if=20((resp->len=20+=20len)=20>=20= MAX_OAUTH_RESPONSE_SIZE)=0A+=09{=0A+=09=09actx_error(actx,=20"response=20= is=20too=20large");=0A+=09=09return=200;=0A+=09}=0A+=0A+=09/*=20The=20= data=20passed=20from=20libcurl=20is=20not=20null-terminated=20*/=0A+=09= appendBinaryPQExpBuffer(resp,=20buf,=20len);=0A+=0A+=09/*=0A+=09=20*=20= Signal=20an=20error=20in=20order=20to=20abort=20the=20transfer=20in=20= case=20we=20ran=20out=20of=0A+=09=20*=20memory=20in=20accepting=20the=20= data.=0A+=09=20*/=0A+=09if=20(PQExpBufferBroken(resp))=0A+=09{=0A+=09=09= actx_error(actx,=20"out=20of=20memory");=0A+=09=09return=200;=0A+=09}=0A= +=0A+=09return=20len;=0A+}=0A+=0A+/*=0A+=20*=20Begins=20an=20HTTP=20= request=20on=20the=20multi=20handle.=20The=20caller=20should=20have=20= set=20up=20all=0A+=20*=20request-specific=20options=20on=20actx->curl=20= first.=20The=20server's=20response=20body=20will=0A+=20*=20be=20= accumulated=20in=20actx->work_data=20(which=20will=20be=20reset,=20so=20= don't=20store=0A+=20*=20anything=20important=20there=20across=20this=20= call).=0A+=20*=0A+=20*=20Once=20a=20request=20is=20queued,=20it=20can=20= be=20driven=20to=20completion=20via=20drive_request().=0A+=20*=20If=20= actx->running=20is=20zero=20upon=20return,=20the=20request=20has=20= already=20finished=20and=0A+=20*=20drive_request()=20can=20be=20called=20= without=20returning=20control=20to=20the=20client.=0A+=20*/=0A+static=20= bool=0A+start_request(struct=20async_ctx=20*actx)=0A+{=0A+=09CURLMcode=09= err;=0A+=0A+=09resetPQExpBuffer(&actx->work_data);=0A+=09= CHECK_SETOPT(actx,=20CURLOPT_WRITEFUNCTION,=20append_data,=20return=20= false);=0A+=09CHECK_SETOPT(actx,=20CURLOPT_WRITEDATA,=20actx,=20return=20= false);=0A+=0A+=09err=20=3D=20curl_multi_add_handle(actx->curlm,=20= actx->curl);=0A+=09if=20(err)=0A+=09{=0A+=09=09actx_error(actx,=20= "failed=20to=20queue=20HTTP=20request:=20%s",=0A+=09=09=09=09=20=20=20= curl_multi_strerror(err));=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09/*=0A= +=09=20*=20actx->running=20tracks=20the=20number=20of=20running=20= handles,=20so=20we=20can=0A+=09=20*=20immediately=20call=20back=20if=20= no=20waiting=20is=20needed.=0A+=09=20*=0A+=09=20*=20Even=20though=20this=20= is=20nominally=20an=20asynchronous=20process,=20there=20are=20some=0A+=09= =20*=20operations=20that=20can=20synchronously=20fail=20by=20this=20= point=20(e.g.=20connections=0A+=09=20*=20to=20closed=20local=20ports)=20= or=20even=20synchronously=20succeed=20if=20the=20stars=20align=0A+=09=20= *=20(all=20the=20libcurl=20connection=20caches=20hit=20and=20the=20= server=20is=20fast).=0A+=09=20*/=0A+=09err=20=3D=20= curl_multi_socket_action(actx->curlm,=20CURL_SOCKET_TIMEOUT,=200,=20= &actx->running);=0A+=09if=20(err)=0A+=09{=0A+=09=09actx_error(actx,=20= "asynchronous=20HTTP=20request=20failed:=20%s",=0A+=09=09=09=09=20=20=20= curl_multi_strerror(err));=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09= return=20true;=0A+}=0A+=0A+/*=0A+=20*=20CURL_IGNORE_DEPRECATION=20was=20= added=20in=207.87.0.=20If=20it's=20not=20defined,=20we=20can=20make=0A+=20= *=20it=20a=20no-op.=0A+=20*/=0A+#ifndef=20CURL_IGNORE_DEPRECATION=0A= +#define=20CURL_IGNORE_DEPRECATION(x)=20x=0A+#endif=0A+=0A+/*=0A+=20*=20= Drives=20the=20multi=20handle=20towards=20completion.=20The=20caller=20= should=20have=20already=0A+=20*=20set=20up=20an=20asynchronous=20request=20= via=20start_request().=0A+=20*/=0A+static=20PostgresPollingStatusType=0A= +drive_request(struct=20async_ctx=20*actx)=0A+{=0A+=09CURLMcode=09err;=0A= +=09CURLMsg=20=20=20=20*msg;=0A+=09int=09=09=09msgs_left;=0A+=09bool=09=09= done;=0A+=0A+=09if=20(actx->running)=0A+=09{=0A+=09=09/*---=0A+=09=09=20= *=20There's=20an=20async=20request=20in=20progress.=20Pump=20the=20multi=20= handle.=0A+=09=09=20*=0A+=09=09=20*=20curl_multi_socket_all()=20is=20= officially=20deprecated,=20because=20it's=0A+=09=09=20*=20inefficient=20= and=20pointless=20if=20your=20event=20loop=20has=20already=20handed=20= you=0A+=09=09=20*=20the=20exact=20sockets=20that=20are=20ready.=20But=20= that's=20not=20our=20use=20case=20--=0A+=09=09=20*=20our=20client=20has=20= no=20way=20to=20tell=20us=20which=20sockets=20are=20ready.=20(They=0A+=09= =09=20*=20don't=20even=20know=20there=20are=20sockets=20to=20begin=20= with.)=0A+=09=09=20*=0A+=09=09=20*=20We=20can=20grab=20the=20list=20of=20= triggered=20events=20from=20the=20multiplexer=0A+=09=09=20*=20ourselves,=20= but=20that's=20effectively=20what=20curl_multi_socket_all()=20is=0A+=09=09= =20*=20going=20to=20do.=20And=20there=20are=20currently=20no=20plans=20= for=20the=20Curl=20project=0A+=09=09=20*=20to=20remove=20or=20break=20= this=20API,=20so=20ignore=20the=20deprecation.=20See=0A+=09=09=20*=0A+=09= =09=20*=20=20=20=20https://curl.se/mail/lib-2024-11/0028.html=0A+=09=09=20= *=0A+=09=09=20*/=0A+=09=09CURL_IGNORE_DEPRECATION(=0A+=09=09=09err=20=3D=20= curl_multi_socket_all(actx->curlm,=20&actx->running);=0A+=09=09)=0A+=0A+=09= =09if=20(err)=0A+=09=09{=0A+=09=09=09actx_error(actx,=20"asynchronous=20= HTTP=20request=20failed:=20%s",=0A+=09=09=09=09=09=20=20=20= curl_multi_strerror(err));=0A+=09=09=09return=20PGRES_POLLING_FAILED;=0A= +=09=09}=0A+=0A+=09=09if=20(actx->running)=0A+=09=09{=0A+=09=09=09/*=20= We'll=20come=20back=20again.=20*/=0A+=09=09=09return=20= PGRES_POLLING_READING;=0A+=09=09}=0A+=09}=0A+=0A+=09done=20=3D=20false;=0A= +=09while=20((msg=20=3D=20curl_multi_info_read(actx->curlm,=20= &msgs_left))=20!=3D=20NULL)=0A+=09{=0A+=09=09if=20(msg->msg=20!=3D=20= CURLMSG_DONE)=0A+=09=09{=0A+=09=09=09/*=0A+=09=09=09=20*=20Future=20= libcurl=20versions=20may=20define=20new=20message=20types;=20we=20don't=0A= +=09=09=09=20*=20know=20how=20to=20handle=20them,=20so=20we'll=20ignore=20= them.=0A+=09=09=09=20*/=0A+=09=09=09continue;=0A+=09=09}=0A+=0A+=09=09/*=20= First=20check=20the=20status=20of=20the=20request=20itself.=20*/=0A+=09=09= if=20(msg->data.result=20!=3D=20CURLE_OK)=0A+=09=09{=0A+=09=09=09/*=0A+=09= =09=09=20*=20If=20a=20more=20specific=20error=20hasn't=20already=20been=20= reported,=20use=0A+=09=09=09=20*=20libcurl's=20description.=0A+=09=09=09=20= */=0A+=09=09=09if=20(actx->errbuf.len=20=3D=3D=200)=0A+=09=09=09=09= actx_error_str(actx,=20curl_easy_strerror(msg->data.result));=0A+=0A+=09=09= =09return=20PGRES_POLLING_FAILED;=0A+=09=09}=0A+=0A+=09=09/*=20Now=20= remove=20the=20finished=20handle;=20we'll=20add=20it=20back=20later=20if=20= needed.=20*/=0A+=09=09err=20=3D=20curl_multi_remove_handle(actx->curlm,=20= msg->easy_handle);=0A+=09=09if=20(err)=0A+=09=09{=0A+=09=09=09= actx_error(actx,=20"libcurl=20easy=20handle=20removal=20failed:=20%s",=0A= +=09=09=09=09=09=20=20=20curl_multi_strerror(err));=0A+=09=09=09return=20= PGRES_POLLING_FAILED;=0A+=09=09}=0A+=0A+=09=09done=20=3D=20true;=0A+=09}=0A= +=0A+=09/*=20Sanity=20check.=20*/=0A+=09if=20(!done)=0A+=09{=0A+=09=09= actx_error(actx,=20"no=20result=20was=20retrieved=20for=20the=20finished=20= handle");=0A+=09=09return=20PGRES_POLLING_FAILED;=0A+=09}=0A+=0A+=09= return=20PGRES_POLLING_OK;=0A+}=0A+=0A+/*=0A+=20*=20URL-Encoding=20= Helpers=0A+=20*/=0A+=0A+/*=0A+=20*=20Encodes=20a=20string=20using=20the=20= application/x-www-form-urlencoded=20format,=20and=0A+=20*=20appends=20it=20= to=20the=20given=20buffer.=0A+=20*/=0A+static=20void=0A= +append_urlencoded(PQExpBuffer=20buf,=20const=20char=20*s)=0A+{=0A+=09= char=09=20=20=20*escaped;=0A+=09char=09=20=20=20*haystack;=0A+=09char=09=20= =20=20*match;=0A+=0A+=09escaped=20=3D=20curl_easy_escape(NULL,=20s,=20= 0);=0A+=09if=20(!escaped)=0A+=09{=0A+=09=09termPQExpBuffer(buf);=09/*=20= mark=20the=20buffer=20broken=20*/=0A+=09=09return;=0A+=09}=0A+=0A+=09/*=0A= +=09=20*=20curl_easy_escape()=20almost=20does=20what=20we=20want,=20but=20= we=20need=20the=0A+=09=20*=20query-specific=20flavor=20which=20uses=20= '+'=20instead=20of=20'%20'=20for=20spaces.=20The=0A+=09=20*=20Curl=20= command-line=20tool=20does=20this=20with=20a=20simple=20= search-and-replace,=20so=0A+=09=20*=20follow=20its=20lead.=0A+=09=20*/=0A= +=09haystack=20=3D=20escaped;=0A+=0A+=09while=20((match=20=3D=20= strstr(haystack,=20"%20"))=20!=3D=20NULL)=0A+=09{=0A+=09=09/*=20Append=20= the=20unmatched=20portion,=20followed=20by=20the=20plus=20sign.=20*/=0A+=09= =09appendBinaryPQExpBuffer(buf,=20haystack,=20match=20-=20haystack);=0A+=09= =09appendPQExpBufferChar(buf,=20'+');=0A+=0A+=09=09/*=20Keep=20searching=20= after=20the=20match.=20*/=0A+=09=09haystack=20=3D=20match=20+=203=20/*=20= strlen("%20")=20*/=20;=0A+=09}=0A+=0A+=09/*=20Push=20the=20remainder=20= of=20the=20string=20onto=20the=20buffer.=20*/=0A+=09= appendPQExpBufferStr(buf,=20haystack);=0A+=0A+=09curl_free(escaped);=0A= +}=0A+=0A+/*=0A+=20*=20Convenience=20wrapper=20for=20encoding=20a=20= single=20string.=20Returns=20NULL=20on=20allocation=0A+=20*=20failure.=0A= +=20*/=0A+static=20char=20*=0A+urlencode(const=20char=20*s)=0A+{=0A+=09= PQExpBufferData=20buf;=0A+=0A+=09initPQExpBuffer(&buf);=0A+=09= append_urlencoded(&buf,=20s);=0A+=0A+=09return=20= PQExpBufferDataBroken(buf)=20?=20NULL=20:=20buf.data;=0A+}=0A+=0A+/*=0A+=20= *=20Appends=20a=20key/value=20pair=20to=20the=20end=20of=20an=20= application/x-www-form-urlencoded=0A+=20*=20list.=0A+=20*/=0A+static=20= void=0A+build_urlencoded(PQExpBuffer=20buf,=20const=20char=20*key,=20= const=20char=20*value)=0A+{=0A+=09if=20(buf->len)=0A+=09=09= appendPQExpBufferChar(buf,=20'&');=0A+=0A+=09append_urlencoded(buf,=20= key);=0A+=09appendPQExpBufferChar(buf,=20'=3D');=0A+=09= append_urlencoded(buf,=20value);=0A+}=0A+=0A+/*=0A+=20*=20Specific=20= HTTP=20Request=20Handlers=0A+=20*=0A+=20*=20This=20is=20finally=20the=20= beginning=20of=20the=20actual=20application=20logic.=20Generally=0A+=20*=20= speaking,=20a=20single=20request=20consists=20of=20a=20start_*=20and=20a=20= finish_*=20step,=20with=0A+=20*=20drive_request()=20pumping=20the=20= machine=20in=20between.=0A+=20*/=0A+=0A+/*=0A+=20*=20Queue=20an=20OpenID=20= Provider=20Configuration=20Request:=0A+=20*=0A+=20*=20=20=20=20=20= https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigu= rationRequest=0A+=20*=20=20=20=20=20= https://www.rfc-editor.org/rfc/rfc8414#section-3.1=0A+=20*=0A+=20*=20= This=20is=20done=20first=20to=20get=20the=20endpoint=20URIs=20we=20need=20= to=20contact=20and=20to=20make=0A+=20*=20sure=20the=20provider=20= provides=20a=20device=20authorization=20flow.=20finish_discovery()=0A+=20= *=20will=20fill=20in=20actx->provider.=0A+=20*/=0A+static=20bool=0A= +start_discovery(struct=20async_ctx=20*actx,=20const=20char=20= *discovery_uri)=0A+{=0A+=09CHECK_SETOPT(actx,=20CURLOPT_HTTPGET,=201L,=20= return=20false);=0A+=09CHECK_SETOPT(actx,=20CURLOPT_URL,=20= discovery_uri,=20return=20false);=0A+=0A+=09return=20= start_request(actx);=0A+}=0A+=0A+static=20bool=0A= +finish_discovery(struct=20async_ctx=20*actx)=0A+{=0A+=09long=09=09= response_code;=0A+=0A+=09/*----=0A+=09=20*=20Now=20check=20the=20= response.=20OIDC=20Discovery=201.0=20is=20pretty=20strict:=0A+=09=20*=0A= +=09=20*=20=20=20=20=20A=20successful=20response=20MUST=20use=20the=20= 200=20OK=20HTTP=20status=20code=20and=0A+=09=20*=20=20=20=20=20return=20= a=20JSON=20object=20using=20the=20application/json=20content=20type=20= that=0A+=09=20*=20=20=20=20=20contains=20a=20set=20of=20Claims=20as=20= its=20members=20that=20are=20a=20subset=20of=20the=0A+=09=20*=20=20=20=20= =20Metadata=20values=20defined=20in=20Section=203.=0A+=09=20*=0A+=09=20*=20= Compared=20to=20standard=20HTTP=20semantics,=20this=20makes=20life=20= easy=20--=20we=20don't=0A+=09=20*=20need=20to=20worry=20about=20= redirections=20(which=20would=20call=20the=20Issuer=20host=0A+=09=20*=20= validation=20into=20question),=20or=20non-authoritative=20responses,=20= or=20any=20other=0A+=09=20*=20complications.=0A+=09=20*/=0A+=09= CHECK_GETINFO(actx,=20CURLINFO_RESPONSE_CODE,=20&response_code,=20return=20= false);=0A+=0A+=09if=20(response_code=20!=3D=20200)=0A+=09{=0A+=09=09= actx_error(actx,=20"unexpected=20response=20code=20%ld",=20= response_code);=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20= Pull=20the=20fields=20we=20care=20about=20from=20the=20document.=0A+=09=20= */=0A+=09actx->errctx=20=3D=20"failed=20to=20parse=20OpenID=20discovery=20= document";=0A+=09if=20(!parse_provider(actx,=20&actx->provider))=0A+=09=09= return=20false;=09=09=09/*=20error=20message=20already=20set=20*/=0A+=0A= +=09/*=0A+=09=20*=20Fill=20in=20any=20defaults=20for=20= OPTIONAL/RECOMMENDED=20fields=20we=20care=20about.=0A+=09=20*/=0A+=09if=20= (!actx->provider.grant_types_supported)=0A+=09{=0A+=09=09/*=0A+=09=09=20= *=20Per=20Section=203,=20the=20default=20is=20["authorization_code",=20= "implicit"].=0A+=09=09=20*/=0A+=09=09struct=20curl_slist=20*temp=20=3D=20= actx->provider.grant_types_supported;=0A+=0A+=09=09temp=20=3D=20= curl_slist_append(temp,=20"authorization_code");=0A+=09=09if=20(temp)=0A= +=09=09{=0A+=09=09=09temp=20=3D=20curl_slist_append(temp,=20"implicit");=0A= +=09=09}=0A+=0A+=09=09if=20(!temp)=0A+=09=09{=0A+=09=09=09= actx_error(actx,=20"out=20of=20memory");=0A+=09=09=09return=20false;=0A+=09= =09}=0A+=0A+=09=09actx->provider.grant_types_supported=20=3D=20temp;=0A+=09= }=0A+=0A+=09return=20true;=0A+}=0A+=0A+/*=0A+=20*=20Ensure=20that=20the=20= discovery=20document=20is=20provided=20by=20the=20expected=20issuer.=0A+=20= *=20Currently,=20issuers=20are=20statically=20configured=20in=20the=20= connection=20string.=0A+=20*/=0A+static=20bool=0A+check_issuer(struct=20= async_ctx=20*actx,=20PGconn=20*conn)=0A+{=0A+=09const=20struct=20= provider=20*provider=20=3D=20&actx->provider;=0A+=0A+=09= Assert(conn->oauth_issuer_id);=09/*=20ensured=20by=20= setup_oauth_parameters()=20*/=0A+=09Assert(provider->issuer);=09/*=20= ensured=20by=20parse_provider()=20*/=0A+=0A+=09/*---=0A+=09=20*=20We=20= require=20strict=20equality=20for=20issuer=20identifiers=20--=20no=20= path=20or=20case=0A+=09=20*=20normalization,=20no=20substitution=20of=20= default=20ports=20and=20schemes,=20etc.=20This=0A+=09=20*=20is=20done=20= to=20match=20the=20rules=20in=20OIDC=20Discovery=20Sec.=204.3=20for=20= config=0A+=09=20*=20validation:=0A+=09=20*=0A+=09=20*=20=20=20=20The=20= issuer=20value=20returned=20MUST=20be=20identical=20to=20the=20Issuer=20= URL=20that=0A+=09=20*=20=20=20=20was=20used=20as=20the=20prefix=20to=20= /.well-known/openid-configuration=20to=0A+=09=20*=20=20=20=20retrieve=20= the=20configuration=20information.=0A+=09=20*=0A+=09=20*=20as=20well=20= as=20the=20rules=20set=20out=20in=20RFC=209207=20for=20avoiding=20mix-up=20= attacks:=0A+=09=20*=0A+=09=20*=20=20=20=20Clients=20MUST=20then=20[...]=20= compare=20the=20result=20to=20the=20issuer=20identifier=0A+=09=20*=20=20=20= =20of=20the=20authorization=20server=20where=20the=20authorization=20= request=20was=0A+=09=20*=20=20=20=20sent=20to.=20This=20comparison=20= MUST=20use=20simple=20string=20comparison=20as=20defined=0A+=09=20*=20=20= =20=20in=20Section=206.2.1=20of=20[RFC3986].=0A+=09=20*/=0A+=09if=20= (strcmp(conn->oauth_issuer_id,=20provider->issuer)=20!=3D=200)=0A+=09{=0A= +=09=09actx_error(actx,=0A+=09=09=09=09=20=20=20"the=20issuer=20= identifier=20(%s)=20does=20not=20match=20oauth_issuer=20(%s)",=0A+=09=09=09= =09=20=20=20provider->issuer,=20conn->oauth_issuer_id);=0A+=09=09return=20= false;=0A+=09}=0A+=0A+=09return=20true;=0A+}=0A+=0A+#define=20= HTTPS_SCHEME=20"https://"=0A+#define=20OAUTH_GRANT_TYPE_DEVICE_CODE=20= "urn:ietf:params:oauth:grant-type:device_code"=0A+=0A+/*=0A+=20*=20= Ensure=20that=20the=20provider=20supports=20the=20Device=20Authorization=20= flow=20(i.e.=20it=0A+=20*=20provides=20an=20authorization=20endpoint,=20= and=20both=20the=20token=20and=20authorization=0A+=20*=20endpoint=20URLs=20= seem=20reasonable).=0A+=20*/=0A+static=20bool=0A= +check_for_device_flow(struct=20async_ctx=20*actx)=0A+{=0A+=09const=20= struct=20provider=20*provider=20=3D=20&actx->provider;=0A+=0A+=09= Assert(provider->issuer);=09/*=20ensured=20by=20parse_provider()=20*/=0A= +=09Assert(provider->token_endpoint);=09/*=20ensured=20by=20= parse_provider()=20*/=0A+=0A+=09if=20= (!provider->device_authorization_endpoint)=0A+=09{=0A+=09=09= actx_error(actx,=0A+=09=09=09=09=20=20=20"issuer=20\"%s\"=20does=20not=20= provide=20a=20device=20authorization=20endpoint",=0A+=09=09=09=09=20=20=20= provider->issuer);=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09/*=0A+=09=20= *=20The=20original=20implementation=20checked=20that=20= OAUTH_GRANT_TYPE_DEVICE_CODE=0A+=09=20*=20was=20present=20in=20the=20= discovery=20document's=20grant_types_supported=20list.=20MS=0A+=09=20*=20= Entra=20does=20not=20advertise=20this=20grant=20type,=20though,=20and=20= since=20it=20doesn't=0A+=09=20*=20make=20sense=20to=20stand=20up=20a=20= device_authorization_endpoint=20without=20also=0A+=09=20*=20accepting=20= device=20codes=20at=20the=20token_endpoint,=20that's=20the=20only=20= thing=20we=0A+=09=20*=20currently=20require.=0A+=09=20*/=0A+=0A+=09/*=0A= +=09=20*=20Although=20libcurl=20will=20fail=20later=20if=20the=20URL=20= contains=20an=20unsupported=0A+=09=20*=20scheme,=20that=20error=20= message=20is=20going=20to=20be=20a=20bit=20opaque.=20This=20is=20a=0A+=09= =20*=20decent=20time=20to=20bail=20out=20if=20we're=20not=20using=20= HTTPS=20for=20the=20endpoints=0A+=09=20*=20we'll=20use=20for=20the=20= flow.=0A+=09=20*/=0A+=09if=20(!actx->debugging)=0A+=09{=0A+=09=09if=20= (pg_strncasecmp(provider->device_authorization_endpoint,=0A+=09=09=09=09=09= =09=20=20=20HTTPS_SCHEME,=20strlen(HTTPS_SCHEME))=20!=3D=200)=0A+=09=09{=0A= +=09=09=09actx_error(actx,=0A+=09=09=09=09=09=20=20=20"device=20= authorization=20endpoint=20\"%s\"=20must=20use=20HTTPS",=0A+=09=09=09=09=09= =20=20=20provider->device_authorization_endpoint);=0A+=09=09=09return=20= false;=0A+=09=09}=0A+=0A+=09=09if=20= (pg_strncasecmp(provider->token_endpoint,=0A+=09=09=09=09=09=09=20=20=20= HTTPS_SCHEME,=20strlen(HTTPS_SCHEME))=20!=3D=200)=0A+=09=09{=0A+=09=09=09= actx_error(actx,=0A+=09=09=09=09=09=20=20=20"token=20endpoint=20\"%s\"=20= must=20use=20HTTPS",=0A+=09=09=09=09=09=20=20=20= provider->token_endpoint);=0A+=09=09=09return=20false;=0A+=09=09}=0A+=09= }=0A+=0A+=09return=20true;=0A+}=0A+=0A+/*=0A+=20*=20Adds=20the=20client=20= ID=20(and=20secret,=20if=20provided)=20to=20the=20current=20request,=20= using=0A+=20*=20either=20HTTP=20headers=20or=20the=20request=20body.=0A+=20= */=0A+static=20bool=0A+add_client_identification(struct=20async_ctx=20= *actx,=20PQExpBuffer=20reqbody,=20PGconn=20*conn)=0A+{=0A+=09bool=09=09= success=20=3D=20false;=0A+=09char=09=20=20=20*username=20=3D=20NULL;=0A+=09= char=09=20=20=20*password=20=3D=20NULL;=0A+=0A+=09if=20= (conn->oauth_client_secret)=09/*=20Zero-length=20secrets=20are=20= permitted!=20*/=0A+=09{=0A+=09=09/*----=0A+=09=09=20*=20Use=20HTTP=20= Basic=20auth=20to=20send=20the=20client_id=20and=20secret.=20Per=20RFC=20= 6749,=0A+=09=09=20*=20Sec.=202.3.1,=0A+=09=09=20*=0A+=09=09=20*=20=20=20= Including=20the=20client=20credentials=20in=20the=20request-body=20using=20= the=0A+=09=09=20*=20=20=20two=20parameters=20is=20NOT=20RECOMMENDED=20= and=20SHOULD=20be=20limited=20to=0A+=09=09=20*=20=20=20clients=20unable=20= to=20directly=20utilize=20the=20HTTP=20Basic=20authentication=0A+=09=09=20= *=20=20=20scheme=20(or=20other=20password-based=20HTTP=20authentication=20= schemes).=0A+=09=09=20*=0A+=09=09=20*=20Additionally:=0A+=09=09=20*=0A+=09= =09=20*=20=20=20The=20client=20identifier=20is=20encoded=20using=20the=0A= +=09=09=20*=20=20=20"application/x-www-form-urlencoded"=20encoding=20= algorithm=20per=20Appendix=0A+=09=09=20*=20=20=20B,=20and=20the=20= encoded=20value=20is=20used=20as=20the=20username;=20the=20client=0A+=09=09= =20*=20=20=20password=20is=20encoded=20using=20the=20same=20algorithm=20= and=20used=20as=20the=0A+=09=09=20*=20=20=20password.=0A+=09=09=20*=0A+=09= =09=20*=20(Appendix=20B=20modifies=20application/x-www-form-urlencoded=20= by=20requiring=0A+=09=09=20*=20an=20initial=20UTF-8=20encoding=20step.=20= Since=20the=20client=20ID=20and=20secret=20must=0A+=09=09=20*=20both=20= be=207-bit=20ASCII=20--=20RFC=206749=20Appendix=20A=20--=20we=20don't=20= worry=20about=0A+=09=09=20*=20that=20in=20this=20function.)=0A+=09=09=20= *=0A+=09=09=20*=20client_id=20is=20not=20added=20to=20the=20request=20= body=20in=20this=20case.=20Not=20only=0A+=09=09=20*=20would=20it=20be=20= redundant,=20but=20some=20providers=20in=20the=20wild=20(e.g.=20Okta)=0A= +=09=09=20*=20refuse=20to=20accept=20it.=0A+=09=09=20*/=0A+=09=09= username=20=3D=20urlencode(conn->oauth_client_id);=0A+=09=09password=20=3D= =20urlencode(conn->oauth_client_secret);=0A+=0A+=09=09if=20(!username=20= ||=20!password)=0A+=09=09{=0A+=09=09=09actx_error(actx,=20"out=20of=20= memory");=0A+=09=09=09goto=20cleanup;=0A+=09=09}=0A+=0A+=09=09= CHECK_SETOPT(actx,=20CURLOPT_HTTPAUTH,=20CURLAUTH_BASIC,=20goto=20= cleanup);=0A+=09=09CHECK_SETOPT(actx,=20CURLOPT_USERNAME,=20username,=20= goto=20cleanup);=0A+=09=09CHECK_SETOPT(actx,=20CURLOPT_PASSWORD,=20= password,=20goto=20cleanup);=0A+=0A+=09=09actx->used_basic_auth=20=3D=20= true;=0A+=09}=0A+=09else=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20If=20we're=20= not=20otherwise=20authenticating,=20client_id=20is=20REQUIRED=20in=20the=0A= +=09=09=20*=20request=20body.=0A+=09=09=20*/=0A+=09=09= build_urlencoded(reqbody,=20"client_id",=20conn->oauth_client_id);=0A+=0A= +=09=09CHECK_SETOPT(actx,=20CURLOPT_HTTPAUTH,=20CURLAUTH_NONE,=20goto=20= cleanup);=0A+=09=09actx->used_basic_auth=20=3D=20false;=0A+=09}=0A+=0A+=09= success=20=3D=20true;=0A+=0A+cleanup:=0A+=09free(username);=0A+=09= free(password);=0A+=0A+=09return=20success;=0A+}=0A+=0A+/*=0A+=20*=20= Queue=20a=20Device=20Authorization=20Request:=0A+=20*=0A+=20*=20=20=20=20= =20https://www.rfc-editor.org/rfc/rfc8628#section-3.1=0A+=20*=0A+=20*=20= This=20is=20the=20second=20step.=20We=20ask=20the=20provider=20to=20= verify=20the=20end=20user=20out=20of=0A+=20*=20band=20and=20authorize=20= us=20to=20act=20on=20their=20behalf;=20it=20will=20give=20us=20the=20= required=0A+=20*=20nonces=20for=20us=20to=20later=20poll=20the=20request=20= status,=20which=20we'll=20grab=20in=0A+=20*=20finish_device_authz().=0A+=20= */=0A+static=20bool=0A+start_device_authz(struct=20async_ctx=20*actx,=20= PGconn=20*conn)=0A+{=0A+=09const=20char=20*device_authz_uri=20=3D=20= actx->provider.device_authorization_endpoint;=0A+=09PQExpBuffer=20= work_buffer=20=3D=20&actx->work_data;=0A+=0A+=09= Assert(conn->oauth_client_id);=09/*=20ensured=20by=20= setup_oauth_parameters()=20*/=0A+=09Assert(device_authz_uri);=09/*=20= ensured=20by=20check_for_device_flow()=20*/=0A+=0A+=09/*=20Construct=20= our=20request=20body.=20*/=0A+=09resetPQExpBuffer(work_buffer);=0A+=09if=20= (conn->oauth_scope=20&&=20conn->oauth_scope[0])=0A+=09=09= build_urlencoded(work_buffer,=20"scope",=20conn->oauth_scope);=0A+=0A+=09= if=20(!add_client_identification(actx,=20work_buffer,=20conn))=0A+=09=09= return=20false;=0A+=0A+=09if=20(PQExpBufferBroken(work_buffer))=0A+=09{=0A= +=09=09actx_error(actx,=20"out=20of=20memory");=0A+=09=09return=20false;=0A= +=09}=0A+=0A+=09/*=20Make=20our=20request.=20*/=0A+=09CHECK_SETOPT(actx,=20= CURLOPT_URL,=20device_authz_uri,=20return=20false);=0A+=09= CHECK_SETOPT(actx,=20CURLOPT_COPYPOSTFIELDS,=20work_buffer->data,=20= return=20false);=0A+=0A+=09return=20start_request(actx);=0A+}=0A+=0A= +static=20bool=0A+finish_device_authz(struct=20async_ctx=20*actx)=0A+{=0A= +=09long=09=09response_code;=0A+=0A+=09CHECK_GETINFO(actx,=20= CURLINFO_RESPONSE_CODE,=20&response_code,=20return=20false);=0A+=0A+=09= /*=0A+=09=20*=20Per=20RFC=208628,=20Section=203,=20a=20successful=20= device=20authorization=20response=0A+=09=20*=20uses=20200=20OK.=0A+=09=20= */=0A+=09if=20(response_code=20=3D=3D=20200)=0A+=09{=0A+=09=09= actx->errctx=20=3D=20"failed=20to=20parse=20device=20authorization";=0A+=09= =09if=20(!parse_device_authz(actx,=20&actx->authz))=0A+=09=09=09return=20= false;=09=09/*=20error=20message=20already=20set=20*/=0A+=0A+=09=09= return=20true;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20The=20device=20= authorization=20endpoint=20uses=20the=20same=20error=20response=20as=20= the=0A+=09=20*=20token=20endpoint,=20so=20the=20error=20handling=20= roughly=20follows=0A+=09=20*=20finish_token_request().=20The=20key=20= difference=20is=20that=20an=20error=20here=20is=0A+=09=20*=20immediately=20= fatal.=0A+=09=20*/=0A+=09if=20(response_code=20=3D=3D=20400=20||=20= response_code=20=3D=3D=20401)=0A+=09{=0A+=09=09struct=20token_error=20= err=20=3D=20{0};=0A+=0A+=09=09if=20(!parse_token_error(actx,=20&err))=0A= +=09=09{=0A+=09=09=09free_token_error(&err);=0A+=09=09=09return=20false;=0A= +=09=09}=0A+=0A+=09=09record_token_error(actx,=20&err);=0A+=0A+=09=09= free_token_error(&err);=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09/*=20= Any=20other=20response=20codes=20are=20considered=20invalid=20*/=0A+=09= actx_error(actx,=20"unexpected=20response=20code=20%ld",=20= response_code);=0A+=09return=20false;=0A+}=0A+=0A+/*=0A+=20*=20Queue=20= an=20Access=20Token=20Request:=0A+=20*=0A+=20*=20=20=20=20=20= https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3=0A+=20*=0A+=20*=20= This=20is=20the=20final=20step.=20We=20continually=20poll=20the=20token=20= endpoint=20to=20see=20if=20the=0A+=20*=20user=20has=20authorized=20us=20= yet.=20finish_token_request()=20will=20pull=20either=20the=20token=0A+=20= *=20or=20a=20(ideally=20temporary)=20error=20status=20from=20the=20= provider.=0A+=20*/=0A+static=20bool=0A+start_token_request(struct=20= async_ctx=20*actx,=20PGconn=20*conn)=0A+{=0A+=09const=20char=20= *token_uri=20=3D=20actx->provider.token_endpoint;=0A+=09const=20char=20= *device_code=20=3D=20actx->authz.device_code;=0A+=09PQExpBuffer=20= work_buffer=20=3D=20&actx->work_data;=0A+=0A+=09= Assert(conn->oauth_client_id);=09/*=20ensured=20by=20= setup_oauth_parameters()=20*/=0A+=09Assert(token_uri);=09=09=09/*=20= ensured=20by=20parse_provider()=20*/=0A+=09Assert(device_code);=09=09/*=20= ensured=20by=20parse_device_authz()=20*/=0A+=0A+=09/*=20Construct=20our=20= request=20body.=20*/=0A+=09resetPQExpBuffer(work_buffer);=0A+=09= build_urlencoded(work_buffer,=20"device_code",=20device_code);=0A+=09= build_urlencoded(work_buffer,=20"grant_type",=20= OAUTH_GRANT_TYPE_DEVICE_CODE);=0A+=0A+=09if=20= (!add_client_identification(actx,=20work_buffer,=20conn))=0A+=09=09= return=20false;=0A+=0A+=09if=20(PQExpBufferBroken(work_buffer))=0A+=09{=0A= +=09=09actx_error(actx,=20"out=20of=20memory");=0A+=09=09return=20false;=0A= +=09}=0A+=0A+=09/*=20Make=20our=20request.=20*/=0A+=09CHECK_SETOPT(actx,=20= CURLOPT_URL,=20token_uri,=20return=20false);=0A+=09CHECK_SETOPT(actx,=20= CURLOPT_COPYPOSTFIELDS,=20work_buffer->data,=20return=20false);=0A+=0A+=09= return=20start_request(actx);=0A+}=0A+=0A+static=20bool=0A= +finish_token_request(struct=20async_ctx=20*actx,=20struct=20token=20= *tok)=0A+{=0A+=09long=09=09response_code;=0A+=0A+=09CHECK_GETINFO(actx,=20= CURLINFO_RESPONSE_CODE,=20&response_code,=20return=20false);=0A+=0A+=09= /*=0A+=09=20*=20Per=20RFC=206749,=20Section=205,=20a=20successful=20= response=20uses=20200=20OK.=0A+=09=20*/=0A+=09if=20(response_code=20=3D=3D= =20200)=0A+=09{=0A+=09=09actx->errctx=20=3D=20"failed=20to=20parse=20= access=20token=20response";=0A+=09=09if=20(!parse_access_token(actx,=20= tok))=0A+=09=09=09return=20false;=09=09/*=20error=20message=20already=20= set=20*/=0A+=0A+=09=09return=20true;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20= An=20error=20response=20uses=20either=20400=20Bad=20Request=20or=20401=20= Unauthorized.=0A+=09=20*=20There=20are=20references=20online=20to=20= implementations=20using=20403=20for=20error=0A+=09=20*=20return=20which=20= would=20violate=20the=20specification.=20For=20now=20we=20stick=20to=20= the=0A+=09=20*=20specification=20but=20we=20might=20have=20to=20revisit=20= this.=0A+=09=20*/=0A+=09if=20(response_code=20=3D=3D=20400=20||=20= response_code=20=3D=3D=20401)=0A+=09{=0A+=09=09if=20= (!parse_token_error(actx,=20&tok->err))=0A+=09=09=09return=20false;=0A+=0A= +=09=09return=20true;=0A+=09}=0A+=0A+=09/*=20Any=20other=20response=20= codes=20are=20considered=20invalid=20*/=0A+=09actx_error(actx,=20= "unexpected=20response=20code=20%ld",=20response_code);=0A+=09return=20= false;=0A+}=0A+=0A+/*=0A+=20*=20Finishes=20the=20token=20request=20and=20= examines=20the=20response.=20If=20the=20flow=20has=0A+=20*=20completed,=20= a=20valid=20token=20will=20be=20returned=20via=20the=20parameter=20list.=20= Otherwise,=0A+=20*=20the=20token=20parameter=20remains=20unchanged,=20= and=20the=20caller=20needs=20to=20wait=20for=0A+=20*=20another=20= interval=20(which=20will=20have=20been=20increased=20in=20response=20to=20= a=20slow_down=0A+=20*=20message=20from=20the=20server)=20before=20= starting=20a=20new=20token=20request.=0A+=20*=0A+=20*=20False=20is=20= returned=20only=20for=20permanent=20error=20conditions.=0A+=20*/=0A= +static=20bool=0A+handle_token_response(struct=20async_ctx=20*actx,=20= char=20**token)=0A+{=0A+=09bool=09=09success=20=3D=20false;=0A+=09struct=20= token=20tok=20=3D=20{0};=0A+=09const=20struct=20token_error=20*err;=0A+=0A= +=09if=20(!finish_token_request(actx,=20&tok))=0A+=09=09goto=20= token_cleanup;=0A+=0A+=09/*=20A=20successful=20token=20request=20gives=20= either=20a=20token=20or=20an=20in-band=20error.=20*/=0A+=09= Assert(tok.access_token=20||=20tok.err.error);=0A+=0A+=09if=20= (tok.access_token)=0A+=09{=0A+=09=09*token=20=3D=20tok.access_token;=0A+=09= =09tok.access_token=20=3D=20NULL;=0A+=0A+=09=09success=20=3D=20true;=0A+=09= =09goto=20token_cleanup;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20= authorization_pending=20and=20slow_down=20are=20the=20only=20acceptable=20= errors;=0A+=09=20*=20anything=20else=20and=20we=20bail.=20These=20are=20= defined=20in=20RFC=208628,=20Sec.=203.5.=0A+=09=20*/=0A+=09err=20=3D=20= &tok.err;=0A+=09if=20(strcmp(err->error,=20"authorization_pending")=20!=3D= =200=20&&=0A+=09=09strcmp(err->error,=20"slow_down")=20!=3D=200)=0A+=09{=0A= +=09=09record_token_error(actx,=20err);=0A+=09=09goto=20token_cleanup;=0A= +=09}=0A+=0A+=09/*=0A+=09=20*=20A=20slow_down=20error=20requires=20us=20= to=20permanently=20increase=20our=20retry=0A+=09=20*=20interval=20by=20= five=20seconds.=0A+=09=20*/=0A+=09if=20(strcmp(err->error,=20= "slow_down")=20=3D=3D=200)=0A+=09{=0A+=09=09int=09=09=09prev_interval=20= =3D=20actx->authz.interval;=0A+=0A+=09=09actx->authz.interval=20+=3D=20= 5;=0A+=09=09if=20(actx->authz.interval=20<=20prev_interval)=0A+=09=09{=0A= +=09=09=09actx_error(actx,=20"slow_down=20interval=20overflow");=0A+=09=09= =09goto=20token_cleanup;=0A+=09=09}=0A+=09}=0A+=0A+=09success=20=3D=20= true;=0A+=0A+token_cleanup:=0A+=09free_token(&tok);=0A+=09return=20= success;=0A+}=0A+=0A+/*=0A+=20*=20Displays=20a=20device=20authorization=20= prompt=20for=20action=20by=20the=20end=20user,=20either=20via=0A+=20*=20= the=20PQauthDataHook,=20or=20by=20a=20message=20on=20standard=20error=20= if=20no=20hook=20is=20set.=0A+=20*/=0A+static=20bool=0A= +prompt_user(struct=20async_ctx=20*actx,=20PGconn=20*conn)=0A+{=0A+=09= int=09=09=09res;=0A+=09PGpromptOAuthDevice=20prompt=20=3D=20{=0A+=09=09= .verification_uri=20=3D=20actx->authz.verification_uri,=0A+=09=09= .user_code=20=3D=20actx->authz.user_code,=0A+=09=09= .verification_uri_complete=20=3D=20= actx->authz.verification_uri_complete,=0A+=09=09.expires_in=20=3D=20= actx->authz.expires_in,=0A+=09};=0A+=0A+=09res=20=3D=20= PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE,=20conn,=20&prompt);=0A+=0A= +=09if=20(!res)=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20translator:=20The=20= first=20%s=20is=20a=20URL=20for=20the=20user=20to=20visit=20in=20a=0A+=09= =09=20*=20browser,=20and=20the=20second=20%s=20is=20a=20code=20to=20be=20= copy-pasted=20there.=0A+=09=09=20*/=0A+=09=09fprintf(stderr,=20= libpq_gettext("Visit=20%s=20and=20enter=20the=20code:=20%s\n"),=0A+=09=09= =09=09prompt.verification_uri,=20prompt.user_code);=0A+=09}=0A+=09else=20= if=20(res=20<=200)=0A+=09{=0A+=09=09actx_error(actx,=20"device=20prompt=20= failed");=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09return=20true;=0A+}=0A= +=0A+/*=0A+=20*=20Calls=20curl_global_init()=20in=20a=20thread-safe=20= way.=0A+=20*=0A+=20*=20libcurl=20has=20stringent=20requirements=20for=20= the=20thread=20context=20in=20which=20you=20call=0A+=20*=20= curl_global_init(),=20because=20it's=20going=20to=20try=20initializing=20= a=20bunch=20of=20other=0A+=20*=20libraries=20(OpenSSL,=20Winsock,=20= etc).=20Recent=20versions=20of=20libcurl=20have=20improved=0A+=20*=20the=20= thread-safety=20situation,=20but=20there's=20a=20chicken-and-egg=20= problem=20at=0A+=20*=20runtime:=20you=20can't=20check=20the=20thread=20= safety=20until=20you've=20initialized=20libcurl,=0A+=20*=20which=20you=20= can't=20do=20from=20within=20a=20thread=20unless=20you=20know=20it's=20= thread-safe...=0A+=20*=0A+=20*=20Returns=20true=20if=20initialization=20= was=20successful.=20Successful=20or=20not,=20this=0A+=20*=20function=20= will=20not=20try=20to=20reinitialize=20Curl=20on=20successive=20calls.=0A= +=20*/=0A+static=20bool=0A+initialize_curl(PGconn=20*conn)=0A+{=0A+=09/*=0A= +=09=20*=20Don't=20let=20the=20compiler=20play=20tricks=20with=20this=20= variable.=20In=20the=0A+=09=20*=20HAVE_THREADSAFE_CURL_GLOBAL_INIT=20= case,=20we=20don't=20care=20if=20two=20threads=0A+=09=20*=20enter=20= simultaneously,=20but=20we=20do=20care=20if=20this=20gets=20set=20= transiently=20to=0A+=09=20*=20PG_BOOL_YES/NO=20in=20cases=20where=20= that's=20not=20the=20final=20answer.=0A+=09=20*/=0A+=09static=20volatile=20= PGTernaryBool=20init_successful=20=3D=20PG_BOOL_UNKNOWN;=0A+#if=20= HAVE_THREADSAFE_CURL_GLOBAL_INIT=0A+=09curl_version_info_data=20*info;=0A= +#endif=0A+=0A+#if=20!HAVE_THREADSAFE_CURL_GLOBAL_INIT=0A+=0A+=09/*=0A+=09= =20*=20Lock=20around=20the=20whole=20function.=20If=20a=20libpq=20client=20= performs=20its=20own=20work=0A+=09=20*=20with=20libcurl,=20it=20must=20= either=20ensure=20that=20Curl=20is=20initialized=20safely=0A+=09=20*=20= before=20calling=20us=20(in=20which=20case=20our=20call=20will=20be=20a=20= no-op),=20or=20else=20it=0A+=09=20*=20must=20guard=20its=20own=20calls=20= to=20curl_global_init()=20with=20a=20registered=0A+=09=20*=20threadlock=20= handler.=20See=20PQregisterThreadLock().=0A+=09=20*/=0A+=09= pglock_thread();=0A+#endif=0A+=0A+=09/*=0A+=09=20*=20Skip=20= initialization=20if=20we've=20already=20done=20it.=20(Curl=20tracks=20= the=20number=0A+=09=20*=20of=20calls;=20there's=20no=20point=20in=20= incrementing=20the=20counter=20every=20time=20we=0A+=09=20*=20connect.)=0A= +=09=20*/=0A+=09if=20(init_successful=20=3D=3D=20PG_BOOL_YES)=0A+=09=09= goto=20done;=0A+=09else=20if=20(init_successful=20=3D=3D=20PG_BOOL_NO)=0A= +=09{=0A+=09=09libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09= "curl_global_init=20previously=20failed=20during=20OAuth=20setup");=0A+=09= =09goto=20done;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20We=20know=20we've=20= already=20initialized=20Winsock=20by=20this=20point=20(see=0A+=09=20*=20= pqMakeEmptyPGconn()),=20so=20we=20should=20be=20able=20to=20safely=20= skip=20that=20bit.=20But=0A+=09=20*=20we=20have=20to=20tell=20libcurl=20= to=20initialize=20everything=20else,=20because=20other=0A+=09=20*=20= pieces=20of=20our=20client=20executable=20may=20already=20be=20using=20= libcurl=20for=20their=0A+=09=20*=20own=20purposes.=20If=20we=20= initialize=20libcurl=20with=20only=20a=20subset=20of=20its=0A+=09=20*=20= features,=20we=20could=20break=20those=20other=20clients=20= nondeterministically,=20and=0A+=09=20*=20that=20would=20probably=20be=20= a=20nightmare=20to=20debug.=0A+=09=20*=0A+=09=20*=20If=20some=20other=20= part=20of=20the=20program=20has=20already=20called=20this,=20it's=20a=0A= +=09=20*=20no-op.=0A+=09=20*/=0A+=09if=20= (curl_global_init(CURL_GLOBAL_ALL=20&=20~CURL_GLOBAL_WIN32)=20!=3D=20= CURLE_OK)=0A+=09{=0A+=09=09libpq_append_conn_error(conn,=0A+=09=09=09=09=09= =09=09=09"curl_global_init=20failed=20during=20OAuth=20setup");=0A+=09=09= init_successful=20=3D=20PG_BOOL_NO;=0A+=09=09goto=20done;=0A+=09}=0A+=0A= +#if=20HAVE_THREADSAFE_CURL_GLOBAL_INIT=0A+=0A+=09/*=0A+=09=20*=20If=20= we=20determined=20at=20configure=20time=20that=20the=20Curl=20= installation=20is=0A+=09=20*=20threadsafe,=20our=20job=20here=20is=20= much=20easier.=20We=20simply=20initialize=20above=0A+=09=20*=20without=20= any=20locking=20(concurrent=20or=20duplicated=20calls=20are=20fine=20in=20= that=0A+=09=20*=20situation),=20then=20double-check=20to=20make=20sure=20= the=20runtime=20setting=20agrees,=0A+=09=20*=20to=20try=20to=20catch=20= silent=20downgrades.=0A+=09=20*/=0A+=09info=20=3D=20= curl_version_info(CURLVERSION_NOW);=0A+=09if=20(!(info->features=20&=20= CURL_VERSION_THREADSAFE))=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20In=20a=20= downgrade=20situation,=20the=20damage=20is=20already=20done.=20Curl=20= global=0A+=09=09=20*=20state=20may=20be=20corrupted.=20Be=20noisy.=0A+=09= =09=20*/=0A+=09=09libpq_append_conn_error(conn,=20"libcurl=20is=20no=20= longer=20threadsafe\n"=0A+=09=09=09=09=09=09=09=09"\tCurl=20= initialization=20was=20reported=20threadsafe=20when=20libpq\n"=0A+=09=09=09= =09=09=09=09=09"\twas=20compiled,=20but=20the=20currently=20installed=20= version=20of\n"=0A+=09=09=09=09=09=09=09=09"\tlibcurl=20reports=20that=20= it=20is=20not.=20Recompile=20libpq=20against\n"=0A+=09=09=09=09=09=09=09=09= "\tthe=20installed=20version=20of=20libcurl.");=0A+=09=09init_successful=20= =3D=20PG_BOOL_NO;=0A+=09=09goto=20done;=0A+=09}=0A+#endif=0A+=0A+=09= init_successful=20=3D=20PG_BOOL_YES;=0A+=0A+done:=0A+#if=20= !HAVE_THREADSAFE_CURL_GLOBAL_INIT=0A+=09pgunlock_thread();=0A+#endif=0A+=09= return=20(init_successful=20=3D=3D=20PG_BOOL_YES);=0A+}=0A+=0A+/*=0A+=20= *=20The=20core=20nonblocking=20libcurl=20implementation.=20This=20will=20= be=20called=20several=0A+=20*=20times=20to=20pump=20the=20async=20= engine.=0A+=20*=0A+=20*=20The=20architecture=20is=20based=20on=20= PQconnectPoll().=20The=20first=20half=20drives=20the=0A+=20*=20= connection=20state=20forward=20as=20necessary,=20returning=20if=20we're=20= not=20ready=20to=0A+=20*=20proceed=20to=20the=20next=20step=20yet.=20The=20= second=20half=20performs=20the=20actual=20transition=0A+=20*=20between=20= states.=0A+=20*=0A+=20*=20You=20can=20trace=20the=20overall=20OAuth=20= flow=20through=20the=20second=20half.=20It's=20linear=0A+=20*=20until=20= we=20get=20to=20the=20end,=20where=20we=20flip=20back=20and=20forth=20= between=0A+=20*=20OAUTH_STEP_TOKEN_REQUEST=20and=20= OAUTH_STEP_WAIT_INTERVAL=20to=20regularly=20ping=20the=0A+=20*=20= provider.=0A+=20*/=0A+static=20PostgresPollingStatusType=0A= +pg_fe_run_oauth_flow_impl(PGconn=20*conn)=0A+{=0A+=09fe_oauth_state=20= *state=20=3D=20conn->sasl_state;=0A+=09struct=20async_ctx=20*actx;=0A+=0A= +=09if=20(!initialize_curl(conn))=0A+=09=09return=20= PGRES_POLLING_FAILED;=0A+=0A+=09if=20(!state->async_ctx)=0A+=09{=0A+=09=09= /*=0A+=09=09=20*=20Create=20our=20asynchronous=20state,=20and=20hook=20= it=20into=20the=20upper-level=0A+=09=09=20*=20OAuth=20state=20= immediately,=20so=20any=20failures=20below=20won't=20leak=20the=0A+=09=09= =20*=20context=20allocation.=0A+=09=09=20*/=0A+=09=09actx=20=3D=20= calloc(1,=20sizeof(*actx));=0A+=09=09if=20(!actx)=0A+=09=09{=0A+=09=09=09= libpq_append_conn_error(conn,=20"out=20of=20memory");=0A+=09=09=09return=20= PGRES_POLLING_FAILED;=0A+=09=09}=0A+=0A+=09=09actx->mux=20=3D=20= PGINVALID_SOCKET;=0A+=09=09actx->timerfd=20=3D=20-1;=0A+=0A+=09=09/*=20= Should=20we=20enable=20unsafe=20features?=20*/=0A+=09=09actx->debugging=20= =3D=20oauth_unsafe_debugging_enabled();=0A+=0A+=09=09state->async_ctx=20= =3D=20actx;=0A+=0A+=09=09initPQExpBuffer(&actx->work_data);=0A+=09=09= initPQExpBuffer(&actx->errbuf);=0A+=0A+=09=09if=20= (!setup_multiplexer(actx))=0A+=09=09=09goto=20error_return;=0A+=0A+=09=09= if=20(!setup_curl_handles(actx))=0A+=09=09=09goto=20error_return;=0A+=09= }=0A+=0A+=09actx=20=3D=20state->async_ctx;=0A+=0A+=09do=0A+=09{=0A+=09=09= /*=20By=20default,=20the=20multiplexer=20is=20the=20altsock.=20Reassign=20= as=20desired.=20*/=0A+=09=09conn->altsock=20=3D=20actx->mux;=0A+=0A+=09=09= switch=20(actx->step)=0A+=09=09{=0A+=09=09=09case=20OAUTH_STEP_INIT:=0A+=09= =09=09=09break;=0A+=0A+=09=09=09case=20OAUTH_STEP_DISCOVERY:=0A+=09=09=09= case=20OAUTH_STEP_DEVICE_AUTHORIZATION:=0A+=09=09=09case=20= OAUTH_STEP_TOKEN_REQUEST:=0A+=09=09=09=09{=0A+=09=09=09=09=09= PostgresPollingStatusType=20status;=0A+=0A+=09=09=09=09=09status=20=3D=20= drive_request(actx);=0A+=0A+=09=09=09=09=09if=20(status=20=3D=3D=20= PGRES_POLLING_FAILED)=0A+=09=09=09=09=09=09goto=20error_return;=0A+=09=09= =09=09=09else=20if=20(status=20!=3D=20PGRES_POLLING_OK)=0A+=09=09=09=09=09= {=0A+=09=09=09=09=09=09/*=20not=20done=20yet=20*/=0A+=09=09=09=09=09=09= return=20status;=0A+=09=09=09=09=09}=0A+=0A+=09=09=09=09=09break;=0A+=09=09= =09=09}=0A+=0A+=09=09=09case=20OAUTH_STEP_WAIT_INTERVAL:=0A+=0A+=09=09=09= =09/*=0A+=09=09=09=09=20*=20The=20client=20application=20is=20supposed=20= to=20wait=20until=20our=20timer=0A+=09=09=09=09=20*=20expires=20before=20= calling=20PQconnectPoll()=20again,=20but=20that=0A+=09=09=09=09=20*=20= might=20not=20happen.=20To=20avoid=20sending=20a=20token=20request=20= early,=0A+=09=09=09=09=20*=20check=20the=20timer=20before=20continuing.=0A= +=09=09=09=09=20*/=0A+=09=09=09=09if=20(!timer_expired(actx))=0A+=09=09=09= =09{=0A+=09=09=09=09=09conn->altsock=20=3D=20actx->timerfd;=0A+=09=09=09=09= =09return=20PGRES_POLLING_READING;=0A+=09=09=09=09}=0A+=0A+=09=09=09=09= /*=20Disable=20the=20expired=20timer.=20*/=0A+=09=09=09=09if=20= (!set_timer(actx,=20-1))=0A+=09=09=09=09=09goto=20error_return;=0A+=0A+=09= =09=09=09break;=0A+=09=09}=0A+=0A+=09=09/*=0A+=09=09=20*=20Each=20case=20= here=20must=20ensure=20that=20actx->running=20is=20set=20while=20we're=0A= +=09=09=20*=20waiting=20on=20some=20asynchronous=20work.=20Most=20cases=20= rely=20on=0A+=09=09=20*=20start_request()=20to=20do=20that=20for=20them.=0A= +=09=09=20*/=0A+=09=09switch=20(actx->step)=0A+=09=09{=0A+=09=09=09case=20= OAUTH_STEP_INIT:=0A+=09=09=09=09actx->errctx=20=3D=20"failed=20to=20= fetch=20OpenID=20discovery=20document";=0A+=09=09=09=09if=20= (!start_discovery(actx,=20conn->oauth_discovery_uri))=0A+=09=09=09=09=09= goto=20error_return;=0A+=0A+=09=09=09=09actx->step=20=3D=20= OAUTH_STEP_DISCOVERY;=0A+=09=09=09=09break;=0A+=0A+=09=09=09case=20= OAUTH_STEP_DISCOVERY:=0A+=09=09=09=09if=20(!finish_discovery(actx))=0A+=09= =09=09=09=09goto=20error_return;=0A+=0A+=09=09=09=09if=20= (!check_issuer(actx,=20conn))=0A+=09=09=09=09=09goto=20error_return;=0A+=0A= +=09=09=09=09actx->errctx=20=3D=20"cannot=20run=20OAuth=20device=20= authorization";=0A+=09=09=09=09if=20(!check_for_device_flow(actx))=0A+=09= =09=09=09=09goto=20error_return;=0A+=0A+=09=09=09=09actx->errctx=20=3D=20= "failed=20to=20obtain=20device=20authorization";=0A+=09=09=09=09if=20= (!start_device_authz(actx,=20conn))=0A+=09=09=09=09=09goto=20= error_return;=0A+=0A+=09=09=09=09actx->step=20=3D=20= OAUTH_STEP_DEVICE_AUTHORIZATION;=0A+=09=09=09=09break;=0A+=0A+=09=09=09= case=20OAUTH_STEP_DEVICE_AUTHORIZATION:=0A+=09=09=09=09if=20= (!finish_device_authz(actx))=0A+=09=09=09=09=09goto=20error_return;=0A+=0A= +=09=09=09=09actx->errctx=20=3D=20"failed=20to=20obtain=20access=20= token";=0A+=09=09=09=09if=20(!start_token_request(actx,=20conn))=0A+=09=09= =09=09=09goto=20error_return;=0A+=0A+=09=09=09=09actx->step=20=3D=20= OAUTH_STEP_TOKEN_REQUEST;=0A+=09=09=09=09break;=0A+=0A+=09=09=09case=20= OAUTH_STEP_TOKEN_REQUEST:=0A+=09=09=09=09if=20= (!handle_token_response(actx,=20&conn->oauth_token))=0A+=09=09=09=09=09= goto=20error_return;=0A+=0A+=09=09=09=09if=20(!actx->user_prompted)=0A+=09= =09=09=09{=0A+=09=09=09=09=09/*=0A+=09=09=09=09=09=20*=20Now=20that=20we=20= know=20the=20token=20endpoint=20isn't=20broken,=20give=0A+=09=09=09=09=09= =20*=20the=20user=20the=20login=20instructions.=0A+=09=09=09=09=09=20*/=0A= +=09=09=09=09=09if=20(!prompt_user(actx,=20conn))=0A+=09=09=09=09=09=09= goto=20error_return;=0A+=0A+=09=09=09=09=09actx->user_prompted=20=3D=20= true;=0A+=09=09=09=09}=0A+=0A+=09=09=09=09if=20(conn->oauth_token)=0A+=09= =09=09=09=09break;=09=09/*=20done!=20*/=0A+=0A+=09=09=09=09/*=0A+=09=09=09= =09=20*=20Wait=20for=20the=20required=20interval=20before=20issuing=20= the=20next=0A+=09=09=09=09=20*=20request.=0A+=09=09=09=09=20*/=0A+=09=09=09= =09if=20(!set_timer(actx,=20actx->authz.interval=20*=201000))=0A+=09=09=09= =09=09goto=20error_return;=0A+=0A+=09=09=09=09/*=0A+=09=09=09=09=20*=20= No=20Curl=20requests=20are=20running,=20so=20we=20can=20simplify=20by=20= having=0A+=09=09=09=09=20*=20the=20client=20wait=20directly=20on=20the=20= timerfd=20rather=20than=20the=0A+=09=09=09=09=20*=20multiplexer.=0A+=09=09= =09=09=20*/=0A+=09=09=09=09conn->altsock=20=3D=20actx->timerfd;=0A+=0A+=09= =09=09=09actx->step=20=3D=20OAUTH_STEP_WAIT_INTERVAL;=0A+=09=09=09=09= actx->running=20=3D=201;=0A+=09=09=09=09break;=0A+=0A+=09=09=09case=20= OAUTH_STEP_WAIT_INTERVAL:=0A+=09=09=09=09actx->errctx=20=3D=20"failed=20= to=20obtain=20access=20token";=0A+=09=09=09=09if=20= (!start_token_request(actx,=20conn))=0A+=09=09=09=09=09goto=20= error_return;=0A+=0A+=09=09=09=09actx->step=20=3D=20= OAUTH_STEP_TOKEN_REQUEST;=0A+=09=09=09=09break;=0A+=09=09}=0A+=0A+=09=09= /*=0A+=09=09=20*=20The=20vast=20majority=20of=20the=20time,=20if=20we=20= don't=20have=20a=20token=20at=20this=0A+=09=09=20*=20point,=20= actx->running=20will=20be=20set.=20But=20there=20are=20some=20corner=20= cases=0A+=09=09=20*=20where=20we=20can=20immediately=20loop=20back=20= around;=20see=20start_request().=0A+=09=09=20*/=0A+=09}=20while=20= (!conn->oauth_token=20&&=20!actx->running);=0A+=0A+=09/*=20If=20we've=20= stored=20a=20token,=20we're=20done.=20Otherwise=20come=20back=20later.=20= */=0A+=09return=20conn->oauth_token=20?=20PGRES_POLLING_OK=20:=20= PGRES_POLLING_READING;=0A+=0A+error_return:=0A+=0A+=09/*=0A+=09=20*=20= Assemble=20the=20three=20parts=20of=20our=20error:=20context,=20body,=20= and=20detail.=20See=0A+=09=20*=20also=20the=20documentation=20for=20= struct=20async_ctx.=0A+=09=20*/=0A+=09if=20(actx->errctx)=0A+=09{=0A+=09=09= appendPQExpBufferStr(&conn->errorMessage,=0A+=09=09=09=09=09=09=09=20= libpq_gettext(actx->errctx));=0A+=09=09= appendPQExpBufferStr(&conn->errorMessage,=20":=20");=0A+=09}=0A+=0A+=09= if=20(PQExpBufferDataBroken(actx->errbuf))=0A+=09=09= appendPQExpBufferStr(&conn->errorMessage,=0A+=09=09=09=09=09=09=09=20= libpq_gettext("out=20of=20memory"));=0A+=09else=0A+=09=09= appendPQExpBufferStr(&conn->errorMessage,=20actx->errbuf.data);=0A+=0A+=09= if=20(actx->curl_err[0])=0A+=09{=0A+=09=09size_t=09=09len;=0A+=0A+=09=09= appendPQExpBuffer(&conn->errorMessage,=20"=20(%s)",=20actx->curl_err);=0A= +=0A+=09=09/*=20Sometimes=20libcurl=20adds=20a=20newline=20to=20the=20= error=20buffer.=20:(=20*/=0A+=09=09len=20=3D=20conn->errorMessage.len;=0A= +=09=09if=20(len=20>=3D=202=20&&=20conn->errorMessage.data[len=20-=202]=20= =3D=3D=20'\n')=0A+=09=09{=0A+=09=09=09conn->errorMessage.data[len=20-=20= 2]=20=3D=20')';=0A+=09=09=09conn->errorMessage.data[len=20-=201]=20=3D=20= '\0';=0A+=09=09=09conn->errorMessage.len--;=0A+=09=09}=0A+=09}=0A+=0A+=09= appendPQExpBufferStr(&conn->errorMessage,=20"\n");=0A+=0A+=09return=20= PGRES_POLLING_FAILED;=0A+}=0A+=0A+/*=0A+=20*=20The=20top-level=20entry=20= point.=20This=20is=20a=20convenient=20place=20to=20put=20necessary=0A+=20= *=20wrapper=20logic=20before=20handing=20off=20to=20the=20true=20= implementation,=20above.=0A+=20*/=0A+PostgresPollingStatusType=0A= +pg_fe_run_oauth_flow(PGconn=20*conn)=0A+{=0A+=09= PostgresPollingStatusType=20result;=0A+#ifndef=20WIN32=0A+=09sigset_t=09= osigset;=0A+=09bool=09=09sigpipe_pending;=0A+=09bool=09=09masked;=0A+=0A= +=09/*---=0A+=09=20*=20Ignore=20SIGPIPE=20on=20this=20thread=20during=20= all=20Curl=20processing.=0A+=09=20*=0A+=09=20*=20Because=20we=20support=20= multiple=20threads,=20we=20have=20to=20set=20up=20libcurl=20with=0A+=09=20= *=20CURLOPT_NOSIGNAL,=20which=20disables=20its=20default=20global=20= handling=20of=0A+=09=20*=20SIGPIPE.=20=46rom=20the=20Curl=20docs:=0A+=09=20= *=0A+=09=20*=20=20=20=20=20libcurl=20makes=20an=20effort=20to=20never=20= cause=20such=20SIGPIPE=20signals=20to=0A+=09=20*=20=20=20=20=20trigger,=20= but=20some=20operating=20systems=20have=20no=20way=20to=20avoid=20them=20= and=0A+=09=20*=20=20=20=20=20even=20on=20those=20that=20have=20there=20= are=20some=20corner=20cases=20when=20they=20may=0A+=09=20*=20=20=20=20=20= still=20happen,=20contrary=20to=20our=20desire.=0A+=09=20*=0A+=09=20*=20= Note=20that=20libcurl=20is=20also=20at=20the=20mercy=20of=20its=20DNS=20= resolution=20and=20SSL=0A+=09=20*=20libraries;=20if=20any=20of=20them=20= forget=20a=20MSG_NOSIGNAL=20then=20we're=20in=20trouble.=0A+=09=20*=20= Modern=20platforms=20and=20libraries=20seem=20to=20get=20it=20right,=20= so=20this=20is=20a=0A+=09=20*=20difficult=20corner=20case=20to=20= exercise=20in=20practice,=20and=20unfortunately=20it's=0A+=09=20*=20not=20= really=20clear=20whether=20it's=20necessary=20in=20all=20cases.=0A+=09=20= */=0A+=09masked=20=3D=20(pq_block_sigpipe(&osigset,=20&sigpipe_pending)=20= =3D=3D=200);=0A+#endif=0A+=0A+=09result=20=3D=20= pg_fe_run_oauth_flow_impl(conn);=0A+=0A+#ifndef=20WIN32=0A+=09if=20= (masked)=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20Undo=20the=20SIGPIPE=20= mask.=20Assume=20we=20may=20have=20gotten=20EPIPE=20(we=20have=20no=0A+=09= =09=20*=20way=20of=20knowing=20at=20this=20level).=0A+=09=09=20*/=0A+=09=09= pq_reset_sigpipe(&osigset,=20sigpipe_pending,=20true=20/*=20EPIPE,=20= maybe=20*/=20);=0A+=09}=0A+#endif=0A+=0A+=09return=20result;=0A+}=0Adiff=20= --git=20a/src/interfaces/libpq/fe-auth-oauth.c=20= b/src/interfaces/libpq/fe-auth-oauth.c=0Anew=20file=20mode=20100644=0A= index=2000000000000..8beae9604c7=0A---=20/dev/null=0A+++=20= b/src/interfaces/libpq/fe-auth-oauth.c=0A@@=20-0,0=20+1,1153=20@@=0A= +/*-----------------------------------------------------------------------= --=0A+=20*=0A+=20*=20fe-auth-oauth.c=0A+=20*=09=20=20=20The=20front-end=20= (client)=20implementation=20of=20OAuth/OIDC=20authentication=0A+=20*=09=20= =20=20using=20the=20SASL=20OAUTHBEARER=20mechanism.=0A+=20*=0A+=20*=20= Portions=20Copyright=20(c)=201996-2024,=20PostgreSQL=20Global=20= Development=20Group=0A+=20*=20Portions=20Copyright=20(c)=201994,=20= Regents=20of=20the=20University=20of=20California=0A+=20*=0A+=20*=20= IDENTIFICATION=0A+=20*=09=20=20src/interfaces/libpq/fe-auth-oauth.c=0A+=20= *=0A+=20= *-------------------------------------------------------------------------= =0A+=20*/=0A+=0A+#include=20"postgres_fe.h"=0A+=0A+#include=20= "common/base64.h"=0A+#include=20"common/hmac.h"=0A+#include=20= "common/jsonapi.h"=0A+#include=20"common/oauth-common.h"=0A+#include=20= "fe-auth.h"=0A+#include=20"fe-auth-oauth.h"=0A+#include=20= "mb/pg_wchar.h"=0A+=0A+/*=20The=20exported=20OAuth=20callback=20= mechanism.=20*/=0A+static=20void=20*oauth_init(PGconn=20*conn,=20const=20= char=20*password,=0A+=09=09=09=09=09=09const=20char=20*sasl_mechanism);=0A= +static=20SASLStatus=20oauth_exchange(void=20*opaq,=20bool=20final,=0A+=09= =09=09=09=09=09=09=09=20char=20*input,=20int=20inputlen,=0A+=09=09=09=09=09= =09=09=09=20char=20**output,=20int=20*outputlen);=0A+static=20bool=20= oauth_channel_bound(void=20*opaq);=0A+static=20void=20oauth_free(void=20= *opaq);=0A+=0A+const=20pg_fe_sasl_mech=20pg_oauth_mech=20=3D=20{=0A+=09= oauth_init,=0A+=09oauth_exchange,=0A+=09oauth_channel_bound,=0A+=09= oauth_free,=0A+};=0A+=0A+/*=0A+=20*=20Initializes=20mechanism=20state=20= for=20OAUTHBEARER.=0A+=20*=0A+=20*=20For=20a=20full=20description=20of=20= the=20API,=20see=20libpq/fe-auth-sasl.h.=0A+=20*/=0A+static=20void=20*=0A= +oauth_init(PGconn=20*conn,=20const=20char=20*password,=0A+=09=09=20=20=20= const=20char=20*sasl_mechanism)=0A+{=0A+=09fe_oauth_state=20*state;=0A+=0A= +=09/*=0A+=09=20*=20We=20only=20support=20one=20SASL=20mechanism=20here;=20= anything=20else=20is=20programmer=0A+=09=20*=20error.=0A+=09=20*/=0A+=09= Assert(sasl_mechanism=20!=3D=20NULL);=0A+=09= Assert(strcmp(sasl_mechanism,=20OAUTHBEARER_NAME)=20=3D=3D=200);=0A+=0A+=09= state=20=3D=20calloc(1,=20sizeof(*state));=0A+=09if=20(!state)=0A+=09=09= return=20NULL;=0A+=0A+=09state->step=20=3D=20FE_OAUTH_INIT;=0A+=09= state->conn=20=3D=20conn;=0A+=0A+=09return=20state;=0A+}=0A+=0A+/*=0A+=20= *=20Frees=20the=20state=20allocated=20by=20oauth_init().=0A+=20*=0A+=20*=20= This=20handles=20only=20mechanism=20state=20tied=20to=20the=20connection=20= lifetime;=20state=0A+=20*=20stored=20in=20state->async_ctx=20is=20freed=20= up=20either=20immediately=20after=20the=0A+=20*=20authentication=20= handshake=20succeeds,=20or=20before=20the=20mechanism=20is=20cleaned=20= up=20on=0A+=20*=20failure.=20See=20pg_fe_cleanup_oauth_flow()=20and=20= cleanup_user_oauth_flow().=0A+=20*/=0A+static=20void=0A+oauth_free(void=20= *opaq)=0A+{=0A+=09fe_oauth_state=20*state=20=3D=20opaq;=0A+=0A+=09/*=20= Any=20async=20authentication=20state=20should=20have=20been=20cleaned=20= up=20already.=20*/=0A+=09Assert(!state->async_ctx);=0A+=0A+=09= free(state);=0A+}=0A+=0A+#define=20kvsep=20"\x01"=0A+=0A+/*=0A+=20*=20= Constructs=20an=20OAUTHBEARER=20client=20initial=20response=20(RFC=20= 7628,=20Sec.=203.1).=0A+=20*=0A+=20*=20If=20discover=20is=20true,=20the=20= initial=20response=20will=20contain=20a=20request=20for=20the=0A+=20*=20= server's=20required=20OAuth=20parameters=20(Sec.=204.3).=20Otherwise,=20= conn->token=20must=0A+=20*=20be=20set;=20it=20will=20be=20sent=20as=20= the=20connection's=20bearer=20token.=0A+=20*=0A+=20*=20Returns=20the=20= response=20as=20a=20null-terminated=20string,=20or=20NULL=20on=20error.=0A= +=20*/=0A+static=20char=20*=0A+client_initial_response(PGconn=20*conn,=20= bool=20discover)=0A+{=0A+=09static=20const=20char=20*const=20resp_format=20= =3D=20"n,,"=20kvsep=20"auth=3D%s%s"=20kvsep=20kvsep;=0A+=0A+=09= PQExpBufferData=20buf;=0A+=09const=20char=20*authn_scheme;=0A+=09char=09=20= =20=20*response=20=3D=20NULL;=0A+=09const=20char=20*token=20=3D=20= conn->oauth_token;=0A+=0A+=09if=20(discover)=0A+=09{=0A+=09=09/*=20= Parameter=20discovery=20uses=20a=20completely=20empty=20auth=20value.=20= */=0A+=09=09authn_scheme=20=3D=20token=20=3D=20"";=0A+=09}=0A+=09else=0A= +=09{=0A+=09=09/*=0A+=09=09=20*=20Use=20a=20Bearer=20authentication=20= scheme=20(RFC=206750,=20Sec.=202.1).=20A=20trailing=0A+=09=09=20*=20= space=20is=20used=20as=20a=20separator.=0A+=09=09=20*/=0A+=09=09= authn_scheme=20=3D=20"Bearer=20";=0A+=0A+=09=09/*=20conn->token=20must=20= have=20been=20set=20in=20this=20case.=20*/=0A+=09=09if=20(!token)=0A+=09=09= {=0A+=09=09=09Assert(false);=0A+=09=09=09libpq_append_conn_error(conn,=0A= +=09=09=09=09=09=09=09=09=09"internal=20error:=20no=20OAuth=20token=20= was=20set=20for=20the=20connection");=0A+=09=09=09return=20NULL;=0A+=09=09= }=0A+=09}=0A+=0A+=09initPQExpBuffer(&buf);=0A+=09appendPQExpBuffer(&buf,=20= resp_format,=20authn_scheme,=20token);=0A+=0A+=09if=20= (!PQExpBufferDataBroken(buf))=0A+=09=09response=20=3D=20= strdup(buf.data);=0A+=09termPQExpBuffer(&buf);=0A+=0A+=09if=20= (!response)=0A+=09=09libpq_append_conn_error(conn,=20"out=20of=20= memory");=0A+=0A+=09return=20response;=0A+}=0A+=0A+/*=0A+=20*=20JSON=20= Parser=20(for=20the=20OAUTHBEARER=20error=20result)=0A+=20*/=0A+=0A+/*=20= Relevant=20JSON=20fields=20in=20the=20error=20result=20object.=20*/=0A= +#define=20ERROR_STATUS_FIELD=20"status"=0A+#define=20ERROR_SCOPE_FIELD=20= "scope"=0A+#define=20ERROR_OPENID_CONFIGURATION_FIELD=20= "openid-configuration"=0A+=0A+struct=20json_ctx=0A+{=0A+=09char=09=20=20=20= *errmsg;=09=09=09/*=20any=20non-NULL=20value=20stops=20all=20processing=20= */=0A+=09PQExpBufferData=20errbuf;=09=09/*=20backing=20memory=20for=20= errmsg=20*/=0A+=09int=09=09=09nested;=09=09=09/*=20nesting=20level=20= (zero=20is=20the=20top)=20*/=0A+=0A+=09const=20char=20= *target_field_name;=09/*=20points=20to=20a=20static=20allocation=20*/=0A= +=09char=09=20=20**target_field;=09/*=20see=20below=20*/=0A+=0A+=09/*=20= target_field,=20if=20set,=20points=20to=20one=20of=20the=20following:=20= */=0A+=09char=09=20=20=20*status;=0A+=09char=09=20=20=20*scope;=0A+=09= char=09=20=20=20*discovery_uri;=0A+};=0A+=0A+#define=20= oauth_json_has_error(ctx)=20\=0A+=09= (PQExpBufferDataBroken((ctx)->errbuf)=20||=20(ctx)->errmsg)=0A+=0A= +#define=20oauth_json_set_error(ctx,=20...)=20\=0A+=09do=20{=20\=0A+=09=09= appendPQExpBuffer(&(ctx)->errbuf,=20__VA_ARGS__);=20\=0A+=09=09= (ctx)->errmsg=20=3D=20(ctx)->errbuf.data;=20\=0A+=09}=20while=20(0)=0A+=0A= +static=20JsonParseErrorType=0A+oauth_json_object_start(void=20*state)=0A= +{=0A+=09struct=20json_ctx=20*ctx=20=3D=20state;=0A+=0A+=09if=20= (ctx->target_field)=0A+=09{=0A+=09=09Assert(ctx->nested=20=3D=3D=201);=0A= +=0A+=09=09oauth_json_set_error(ctx,=0A+=09=09=09=09=09=09=09=20= libpq_gettext("field=20\"%s\"=20must=20be=20a=20string"),=0A+=09=09=09=09= =09=09=09=20ctx->target_field_name);=0A+=09}=0A+=0A+=09++ctx->nested;=0A= +=09return=20oauth_json_has_error(ctx)=20?=20JSON_SEM_ACTION_FAILED=20:=20= JSON_SUCCESS;=0A+}=0A+=0A+static=20JsonParseErrorType=0A= +oauth_json_object_end(void=20*state)=0A+{=0A+=09struct=20json_ctx=20= *ctx=20=3D=20state;=0A+=0A+=09--ctx->nested;=0A+=09return=20= JSON_SUCCESS;=0A+}=0A+=0A+static=20JsonParseErrorType=0A= +oauth_json_object_field_start(void=20*state,=20char=20*name,=20bool=20= isnull)=0A+{=0A+=09struct=20json_ctx=20*ctx=20=3D=20state;=0A+=0A+=09/*=20= Only=20top-level=20keys=20are=20considered.=20*/=0A+=09if=20(ctx->nested=20= =3D=3D=201)=0A+=09{=0A+=09=09if=20(strcmp(name,=20ERROR_STATUS_FIELD)=20= =3D=3D=200)=0A+=09=09{=0A+=09=09=09ctx->target_field_name=20=3D=20= ERROR_STATUS_FIELD;=0A+=09=09=09ctx->target_field=20=3D=20&ctx->status;=0A= +=09=09}=0A+=09=09else=20if=20(strcmp(name,=20ERROR_SCOPE_FIELD)=20=3D=3D=20= 0)=0A+=09=09{=0A+=09=09=09ctx->target_field_name=20=3D=20= ERROR_SCOPE_FIELD;=0A+=09=09=09ctx->target_field=20=3D=20&ctx->scope;=0A= +=09=09}=0A+=09=09else=20if=20(strcmp(name,=20= ERROR_OPENID_CONFIGURATION_FIELD)=20=3D=3D=200)=0A+=09=09{=0A+=09=09=09= ctx->target_field_name=20=3D=20ERROR_OPENID_CONFIGURATION_FIELD;=0A+=09=09= =09ctx->target_field=20=3D=20&ctx->discovery_uri;=0A+=09=09}=0A+=09}=0A+=0A= +=09return=20JSON_SUCCESS;=0A+}=0A+=0A+static=20JsonParseErrorType=0A= +oauth_json_array_start(void=20*state)=0A+{=0A+=09struct=20json_ctx=20= *ctx=20=3D=20state;=0A+=0A+=09if=20(!ctx->nested)=0A+=09{=0A+=09=09= ctx->errmsg=20=3D=20libpq_gettext("top-level=20element=20must=20be=20an=20= object");=0A+=09}=0A+=09else=20if=20(ctx->target_field)=0A+=09{=0A+=09=09= Assert(ctx->nested=20=3D=3D=201);=0A+=0A+=09=09oauth_json_set_error(ctx,=0A= +=09=09=09=09=09=09=09=20libpq_gettext("field=20\"%s\"=20must=20be=20a=20= string"),=0A+=09=09=09=09=09=09=09=20ctx->target_field_name);=0A+=09}=0A= +=0A+=09return=20oauth_json_has_error(ctx)=20?=20JSON_SEM_ACTION_FAILED=20= :=20JSON_SUCCESS;=0A+}=0A+=0A+static=20JsonParseErrorType=0A= +oauth_json_scalar(void=20*state,=20char=20*token,=20JsonTokenType=20= type)=0A+{=0A+=09struct=20json_ctx=20*ctx=20=3D=20state;=0A+=0A+=09if=20= (!ctx->nested)=0A+=09{=0A+=09=09ctx->errmsg=20=3D=20= libpq_gettext("top-level=20element=20must=20be=20an=20object");=0A+=09=09= return=20JSON_SEM_ACTION_FAILED;=0A+=09}=0A+=0A+=09if=20= (ctx->target_field)=0A+=09{=0A+=09=09if=20(ctx->nested=20!=3D=201)=0A+=09= =09{=0A+=09=09=09/*=0A+=09=09=09=20*=20ctx->target_field=20should=20not=20= have=20been=20set=20for=20nested=20keys.=0A+=09=09=09=20*=20Assert=20and=20= don't=20continue=20any=20further=20for=20production=20builds.=0A+=09=09=09= =20*/=0A+=09=09=09Assert(false);=0A+=09=09=09oauth_json_set_error(ctx,=09= /*=20don't=20bother=20translating=20*/=0A+=09=09=09=09=09=09=09=09=20= "internal=20error:=20target=20scalar=20found=20at=20nesting=20level=20%d=20= during=20OAUTHBEARER=20parsing",=0A+=09=09=09=09=09=09=09=09=20= ctx->nested);=0A+=09=09=09return=20JSON_SEM_ACTION_FAILED;=0A+=09=09}=0A= +=0A+=09=09/*=0A+=09=09=20*=20We=20don't=20allow=20duplicate=20field=20= names;=20error=20out=20if=20the=20target=20has=0A+=09=09=20*=20already=20= been=20set.=0A+=09=09=20*/=0A+=09=09if=20(*ctx->target_field)=0A+=09=09{=0A= +=09=09=09oauth_json_set_error(ctx,=0A+=09=09=09=09=09=09=09=09=20= libpq_gettext("field=20\"%s\"=20is=20duplicated"),=0A+=09=09=09=09=09=09=09= =09=20ctx->target_field_name);=0A+=09=09=09return=20= JSON_SEM_ACTION_FAILED;=0A+=09=09}=0A+=0A+=09=09/*=20The=20only=20fields=20= we=20support=20are=20strings.=20*/=0A+=09=09if=20(type=20!=3D=20= JSON_TOKEN_STRING)=0A+=09=09{=0A+=09=09=09oauth_json_set_error(ctx,=0A+=09= =09=09=09=09=09=09=09=20libpq_gettext("field=20\"%s\"=20must=20be=20a=20= string"),=0A+=09=09=09=09=09=09=09=09=20ctx->target_field_name);=0A+=09=09= =09return=20JSON_SEM_ACTION_FAILED;=0A+=09=09}=0A+=0A+=09=09= *ctx->target_field=20=3D=20strdup(token);=0A+=09=09if=20= (!*ctx->target_field)=0A+=09=09=09return=20JSON_OUT_OF_MEMORY;=0A+=0A+=09= =09ctx->target_field=20=3D=20NULL;=0A+=09=09ctx->target_field_name=20=3D=20= NULL;=0A+=09}=0A+=09else=0A+=09{=0A+=09=09/*=20otherwise=20we=20just=20= ignore=20it=20*/=0A+=09}=0A+=0A+=09return=20JSON_SUCCESS;=0A+}=0A+=0A= +#define=20HTTPS_SCHEME=20"https://"=0A+#define=20HTTP_SCHEME=20= "http://"=0A+=0A+/*=20We=20support=20both=20well-known=20suffixes=20= defined=20by=20RFC=208414.=20*/=0A+#define=20WK_PREFIX=20"/.well-known/"=0A= +#define=20OPENID_WK_SUFFIX=20"openid-configuration"=0A+#define=20= OAUTH_WK_SUFFIX=20"oauth-authorization-server"=0A+=0A+/*=0A+=20*=20= Derives=20an=20issuer=20identifier=20from=20one=20of=20our=20recognized=20= .well-known=20URIs,=0A+=20*=20using=20the=20rules=20in=20RFC=208414.=0A+=20= */=0A+static=20char=20*=0A+issuer_from_well_known_uri(PGconn=20*conn,=20= const=20char=20*wkuri)=0A+{=0A+=09const=20char=20*authority_start=20=3D=20= NULL;=0A+=09const=20char=20*wk_start;=0A+=09const=20char=20*wk_end;=0A+=09= char=09=20=20=20*issuer;=0A+=09ptrdiff_t=09start_offset,=0A+=09=09=09=09= end_offset;=0A+=09size_t=09=09end_len;=0A+=0A+=09/*=0A+=09=20*=20= https://=20is=20required=20for=20issuer=20identifiers=20(RFC=208414,=20= Sec.=202;=20OIDC=0A+=09=20*=20Discovery=201.0,=20Sec.=203).=20This=20is=20= a=20case-insensitive=20comparison=20at=20this=0A+=09=20*=20level=20(but=20= issuer=20identifier=20comparison=20at=20the=20level=20above=20this=20is=0A= +=09=20*=20case-sensitive,=20so=20in=20practice=20it's=20probably=20= moot).=0A+=09=20*/=0A+=09if=20(pg_strncasecmp(wkuri,=20HTTPS_SCHEME,=20= strlen(HTTPS_SCHEME))=20=3D=3D=200)=0A+=09=09authority_start=20=3D=20= wkuri=20+=20strlen(HTTPS_SCHEME);=0A+=0A+=09if=20(!authority_start=0A+=09= =09&&=20oauth_unsafe_debugging_enabled()=0A+=09=09&&=20= pg_strncasecmp(wkuri,=20HTTP_SCHEME,=20strlen(HTTP_SCHEME))=20=3D=3D=20= 0)=0A+=09{=0A+=09=09/*=20Allow=20http://=20for=20testing=20only.=20*/=0A= +=09=09authority_start=20=3D=20wkuri=20+=20strlen(HTTP_SCHEME);=0A+=09}=0A= +=0A+=09if=20(!authority_start)=0A+=09{=0A+=09=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09"OAuth=20= discovery=20URI=20\"%s\"=20must=20use=20HTTPS",=0A+=09=09=09=09=09=09=09=09= wkuri);=0A+=09=09return=20NULL;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20= Well-known=20URIs=20in=20general=20may=20support=20queries=20and=20= fragments,=20but=20the=0A+=09=20*=20two=20types=20we=20support=20here=20= do=20not.=20(They=20must=20be=20constructed=20from=20the=0A+=09=20*=20= components=20of=20issuer=20identifiers,=20which=20themselves=20may=20not=20= contain=20any=0A+=09=20*=20queries=20or=20fragments.)=0A+=09=20*=0A+=09=20= *=20It's=20important=20to=20check=20this=20first,=20to=20avoid=20getting=20= tricked=20later=20by=20a=0A+=09=20*=20prefix=20buried=20inside=20a=20= query=20or=20fragment.=0A+=09=20*/=0A+=09if=20(strpbrk(authority_start,=20= "?#")=20!=3D=20NULL)=0A+=09{=0A+=09=09libpq_append_conn_error(conn,=0A+=09= =09=09=09=09=09=09=09"OAuth=20discovery=20URI=20\"%s\"=20must=20not=20= contain=20query=20or=20fragment=20components",=0A+=09=09=09=09=09=09=09=09= wkuri);=0A+=09=09return=20NULL;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20Find=20= the=20start=20of=20the=20.well-known=20prefix.=20IETF=20rules=20(RFC=20= 8615)=20state=0A+=09=20*=20this=20must=20be=20at=20the=20beginning=20of=20= the=20path=20component,=20but=20OIDC=20defined=0A+=09=20*=20it=20at=20= the=20end=20instead=20(OIDC=20Discovery=201.0,=20Sec.=204),=20so=20we=20= have=20to=0A+=09=20*=20search=20for=20it=20anywhere.=0A+=09=20*/=0A+=09= wk_start=20=3D=20strstr(authority_start,=20WK_PREFIX);=0A+=09if=20= (!wk_start)=0A+=09{=0A+=09=09libpq_append_conn_error(conn,=0A+=09=09=09=09= =09=09=09=09"OAuth=20discovery=20URI=20\"%s\"=20is=20not=20a=20= .well-known=20URI",=0A+=09=09=09=09=09=09=09=09wkuri);=0A+=09=09return=20= NULL;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20Now=20find=20the=20suffix=20= type.=20We=20only=20support=20the=20two=20defined=20in=20OIDC=0A+=09=20*=20= Discovery=201.0=20and=20RFC=208414.=0A+=09=20*/=0A+=09wk_end=20=3D=20= wk_start=20+=20strlen(WK_PREFIX);=0A+=0A+=09if=20(strncmp(wk_end,=20= OPENID_WK_SUFFIX,=20strlen(OPENID_WK_SUFFIX))=20=3D=3D=200)=0A+=09=09= wk_end=20+=3D=20strlen(OPENID_WK_SUFFIX);=0A+=09else=20if=20= (strncmp(wk_end,=20OAUTH_WK_SUFFIX,=20strlen(OAUTH_WK_SUFFIX))=20=3D=3D=20= 0)=0A+=09=09wk_end=20+=3D=20strlen(OAUTH_WK_SUFFIX);=0A+=09else=0A+=09=09= wk_end=20=3D=20NULL;=0A+=0A+=09/*=0A+=09=20*=20Even=20if=20there's=20a=20= match,=20we=20still=20need=20to=20check=20to=20make=20sure=20the=20= suffix=0A+=09=20*=20takes=20up=20the=20entire=20path=20segment,=20to=20= weed=20out=20constructions=20like=0A+=09=20*=20= "/.well-known/openid-configuration-bad".=0A+=09=20*/=0A+=09if=20(!wk_end=20= ||=20(*wk_end=20!=3D=20'/'=20&&=20*wk_end=20!=3D=20'\0'))=0A+=09{=0A+=09=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09"OAuth=20= discovery=20URI=20\"%s\"=20uses=20an=20unsupported=20.well-known=20= suffix",=0A+=09=09=09=09=09=09=09=09wkuri);=0A+=09=09return=20NULL;=0A+=09= }=0A+=0A+=09/*=0A+=09=20*=20Finally,=20make=20sure=20the=20.well-known=20= components=20are=20provided=20either=20as=20a=0A+=09=20*=20prefix=20= (IETF=20style)=20or=20as=20a=20postfix=20(OIDC=20style).=20In=20other=20= words,=0A+=09=20*=20= "https://localhost/a/.well-known/openid-configuration/b"=20is=20not=20= allowed=0A+=09=20*=20to=20claim=20association=20with=20= "https://localhost/a/b".=0A+=09=20*/=0A+=09if=20(*wk_end=20!=3D=20'\0')=0A= +=09{=0A+=09=09/*=0A+=09=09=20*=20It's=20not=20at=20the=20end,=20so=20= it's=20required=20to=20be=20at=20the=20beginning=20at=20the=0A+=09=09=20= *=20path.=20Find=20the=20starting=20slash.=0A+=09=09=20*/=0A+=09=09const=20= char=20*path_start;=0A+=0A+=09=09path_start=20=3D=20= strchr(authority_start,=20'/');=0A+=09=09Assert(path_start);=09=09/*=20= otherwise=20we=20wouldn't=20have=20found=20WK_PREFIX=20*/=0A+=0A+=09=09= if=20(wk_start=20!=3D=20path_start)=0A+=09=09{=0A+=09=09=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09=09"OAuth=20= discovery=20URI=20\"%s\"=20uses=20an=20invalid=20format",=0A+=09=09=09=09= =09=09=09=09=09wkuri);=0A+=09=09=09return=20NULL;=0A+=09=09}=0A+=09}=0A+=0A= +=09/*=20Checks=20passed!=20Now=20build=20the=20issuer.=20*/=0A+=09= issuer=20=3D=20strdup(wkuri);=0A+=09if=20(!issuer)=0A+=09{=0A+=09=09= libpq_append_conn_error(conn,=20"out=20of=20memory");=0A+=09=09return=20= NULL;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20The=20.well-known=20components=20= are=20from=20[wk_start,=20wk_end).=20Remove=20those=20to=0A+=09=20*=20= form=20the=20issuer=20ID,=20by=20shifting=20the=20path=20suffix=20(which=20= may=20be=20empty)=0A+=09=20*=20leftwards.=0A+=09=20*/=0A+=09start_offset=20= =3D=20wk_start=20-=20wkuri;=0A+=09end_offset=20=3D=20wk_end=20-=20wkuri;=0A= +=09end_len=20=3D=20strlen(wk_end)=20+=201;=09/*=20move=20the=20NULL=20= terminator=20too=20*/=0A+=0A+=09memmove(issuer=20+=20start_offset,=20= issuer=20+=20end_offset,=20end_len);=0A+=0A+=09return=20issuer;=0A+}=0A+=0A= +/*=0A+=20*=20Parses=20the=20server=20error=20result=20(RFC=207628,=20= Sec.=203.2.2)=20contained=20in=20msg=20and=0A+=20*=20stores=20any=20= discovered=20openid_configuration=20and=20scope=20settings=20for=20the=0A= +=20*=20connection.=0A+=20*/=0A+static=20bool=0A= +handle_oauth_sasl_error(PGconn=20*conn,=20const=20char=20*msg,=20int=20= msglen)=0A+{=0A+=09JsonLexContext=20lex=20=3D=20{0};=0A+=09JsonSemAction=20= sem=20=3D=20{0};=0A+=09JsonParseErrorType=20err;=0A+=09struct=20json_ctx=20= ctx=20=3D=20{0};=0A+=09char=09=20=20=20*errmsg=20=3D=20NULL;=0A+=09bool=09= =09success=20=3D=20false;=0A+=0A+=09Assert(conn->oauth_issuer_id);=09/*=20= ensured=20by=20setup_oauth_parameters()=20*/=0A+=0A+=09/*=20Sanity=20= check.=20*/=0A+=09if=20(strlen(msg)=20!=3D=20msglen)=0A+=09{=0A+=09=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09"server's=20= error=20message=20contained=20an=20embedded=20NULL,=20and=20was=20= discarded");=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20= pg_parse_json=20doesn't=20validate=20the=20incoming=20UTF-8,=20so=20we=20= have=20to=20check=0A+=09=20*=20that=20up=20front.=0A+=09=20*/=0A+=09if=20= (pg_encoding_verifymbstr(PG_UTF8,=20msg,=20msglen)=20!=3D=20msglen)=0A+=09= {=0A+=09=09libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09= "server's=20error=20response=20is=20not=20valid=20UTF-8");=0A+=09=09= return=20false;=0A+=09}=0A+=0A+=09makeJsonLexContextCstringLen(&lex,=20= msg,=20msglen,=20PG_UTF8,=20true);=0A+=09= setJsonLexContextOwnsTokens(&lex,=20true);=09/*=20must=20not=20leak=20on=20= error=20*/=0A+=0A+=09initPQExpBuffer(&ctx.errbuf);=0A+=09sem.semstate=20= =3D=20&ctx;=0A+=0A+=09sem.object_start=20=3D=20oauth_json_object_start;=0A= +=09sem.object_end=20=3D=20oauth_json_object_end;=0A+=09= sem.object_field_start=20=3D=20oauth_json_object_field_start;=0A+=09= sem.array_start=20=3D=20oauth_json_array_start;=0A+=09sem.scalar=20=3D=20= oauth_json_scalar;=0A+=0A+=09err=20=3D=20pg_parse_json(&lex,=20&sem);=0A= +=0A+=09if=20(err=20=3D=3D=20JSON_SEM_ACTION_FAILED)=0A+=09{=0A+=09=09if=20= (PQExpBufferDataBroken(ctx.errbuf))=0A+=09=09=09errmsg=20=3D=20= libpq_gettext("out=20of=20memory");=0A+=09=09else=20if=20(ctx.errmsg)=0A= +=09=09=09errmsg=20=3D=20ctx.errmsg;=0A+=09=09else=0A+=09=09{=0A+=09=09=09= /*=0A+=09=09=09=20*=20Developer=20error:=20one=20of=20the=20action=20= callbacks=20didn't=20call=0A+=09=09=09=20*=20oauth_json_set_error()=20= before=20erroring=20out.=0A+=09=09=09=20*/=0A+=09=09=09= Assert(oauth_json_has_error(&ctx));=0A+=09=09=09errmsg=20=3D=20= "";=0A+=09=09}=0A+=09}=0A+=09else=20if=20= (err=20!=3D=20JSON_SUCCESS)=0A+=09=09errmsg=20=3D=20json_errdetail(err,=20= &lex);=0A+=0A+=09if=20(errmsg)=0A+=09=09libpq_append_conn_error(conn,=0A= +=09=09=09=09=09=09=09=09"failed=20to=20parse=20server's=20error=20= response:=20%s",=0A+=09=09=09=09=09=09=09=09errmsg);=0A+=0A+=09/*=20= Don't=20need=20the=20error=20buffer=20or=20the=20JSON=20lexer=20anymore.=20= */=0A+=09termPQExpBuffer(&ctx.errbuf);=0A+=09freeJsonLexContext(&lex);=0A= +=0A+=09if=20(errmsg)=0A+=09=09goto=20cleanup;=0A+=0A+=09if=20= (ctx.discovery_uri)=0A+=09{=0A+=09=09char=09=20=20=20*discovery_issuer;=0A= +=0A+=09=09/*=0A+=09=09=20*=20The=20URI=20MUST=20correspond=20to=20our=20= existing=20issuer,=20to=20avoid=20mix-ups.=0A+=09=09=20*=0A+=09=09=20*=20= Issuer=20comparison=20is=20done=20byte-wise,=20rather=20than=20= performing=20any=20URL=0A+=09=09=20*=20normalization;=20this=20follows=20= the=20suggestions=20for=20issuer=20comparison=0A+=09=09=20*=20in=20RFC=20= 9207=20Sec.=202.4=20(which=20requires=20simple=20string=20comparison)=20= and=0A+=09=09=20*=20vastly=20simplifies=20things.=20Since=20this=20is=20= the=20key=20protection=20against=0A+=09=09=20*=20a=20rogue=20server=20= sending=20the=20client=20to=20an=20untrustworthy=20location,=0A+=09=09=20= *=20simpler=20is=20better.=0A+=09=09=20*/=0A+=09=09discovery_issuer=20=3D=20= issuer_from_well_known_uri(conn,=20ctx.discovery_uri);=0A+=09=09if=20= (!discovery_issuer)=0A+=09=09=09goto=20cleanup;=09=09/*=20error=20= message=20already=20set=20*/=0A+=0A+=09=09if=20= (strcmp(conn->oauth_issuer_id,=20discovery_issuer)=20!=3D=200)=0A+=09=09= {=0A+=09=09=09libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09=09= "server's=20discovery=20document=20at=20%s=20(issuer=20\"%s\")=20is=20= incompatible=20with=20oauth_issuer=20(%s)",=0A+=09=09=09=09=09=09=09=09=09= ctx.discovery_uri,=20discovery_issuer,=0A+=09=09=09=09=09=09=09=09=09= conn->oauth_issuer_id);=0A+=0A+=09=09=09free(discovery_issuer);=0A+=09=09= =09goto=20cleanup;=0A+=09=09}=0A+=0A+=09=09free(discovery_issuer);=0A+=0A= +=09=09if=20(!conn->oauth_discovery_uri)=0A+=09=09{=0A+=09=09=09= conn->oauth_discovery_uri=20=3D=20ctx.discovery_uri;=0A+=09=09=09= ctx.discovery_uri=20=3D=20NULL;=0A+=09=09}=0A+=09=09else=0A+=09=09{=0A+=09= =09=09/*=20This=20must=20match=20the=20URI=20we'd=20previously=20= determined.=20*/=0A+=09=09=09if=20(strcmp(conn->oauth_discovery_uri,=20= ctx.discovery_uri)=20!=3D=200)=0A+=09=09=09{=0A+=09=09=09=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09=09=09"server's=20= discovery=20document=20has=20moved=20to=20%s=20(previous=20location=20= was=20%s)",=0A+=09=09=09=09=09=09=09=09=09=09ctx.discovery_uri,=0A+=09=09= =09=09=09=09=09=09=09=09conn->oauth_discovery_uri);=0A+=09=09=09=09goto=20= cleanup;=0A+=09=09=09}=0A+=09=09}=0A+=09}=0A+=0A+=09if=20(ctx.scope)=0A+=09= {=0A+=09=09/*=20Servers=20may=20not=20override=20a=20previously=20set=20= oauth_scope.=20*/=0A+=09=09if=20(!conn->oauth_scope)=0A+=09=09{=0A+=09=09= =09conn->oauth_scope=20=3D=20ctx.scope;=0A+=09=09=09ctx.scope=20=3D=20= NULL;=0A+=09=09}=0A+=09}=0A+=0A+=09if=20(!ctx.status)=0A+=09{=0A+=09=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09"server=20sent=20= error=20response=20without=20a=20status");=0A+=09=09goto=20cleanup;=0A+=09= }=0A+=0A+=09if=20(strcmp(ctx.status,=20"invalid_token")=20!=3D=200)=0A+=09= {=0A+=09=09/*=0A+=09=09=20*=20invalid_token=20is=20the=20only=20error=20= code=20we'll=20automatically=20retry=20for;=0A+=09=09=20*=20otherwise,=20= just=20bail=20out=20now.=0A+=09=09=20*/=0A+=09=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09"server=20= rejected=20OAuth=20bearer=20token:=20%s",=0A+=09=09=09=09=09=09=09=09= ctx.status);=0A+=09=09goto=20cleanup;=0A+=09}=0A+=0A+=09success=20=3D=20= true;=0A+=0A+cleanup:=0A+=09free(ctx.status);=0A+=09free(ctx.scope);=0A+=09= free(ctx.discovery_uri);=0A+=0A+=09return=20success;=0A+}=0A+=0A+/*=0A+=20= *=20Callback=20implementation=20of=20conn->async_auth()=20for=20a=20= user-defined=20OAuth=20flow.=0A+=20*=20Delegates=20the=20retrieval=20of=20= the=20token=20to=20the=20application's=20async=20callback.=0A+=20*=0A+=20= *=20This=20will=20be=20called=20multiple=20times=20as=20needed;=20the=20= application=20is=20responsible=0A+=20*=20for=20setting=20an=20altsock=20= to=20signal=20and=20returning=20the=20correct=20PGRES_POLLING_*=0A+=20*=20= statuses=20for=20use=20by=20PQconnectPoll().=0A+=20*/=0A+static=20= PostgresPollingStatusType=0A+run_user_oauth_flow(PGconn=20*conn)=0A+{=0A= +=09fe_oauth_state=20*state=20=3D=20conn->sasl_state;=0A+=09= PGoauthBearerRequest=20*request=20=3D=20state->async_ctx;=0A+=09= PostgresPollingStatusType=20status;=0A+=0A+=09if=20(!request->async)=0A+=09= {=0A+=09=09libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09= "user-defined=20OAuth=20flow=20provided=20neither=20a=20token=20nor=20an=20= async=20callback");=0A+=09=09return=20PGRES_POLLING_FAILED;=0A+=09}=0A+=0A= +=09status=20=3D=20request->async(conn,=20request,=20&conn->altsock);=0A= +=09if=20(status=20=3D=3D=20PGRES_POLLING_FAILED)=0A+=09{=0A+=09=09= libpq_append_conn_error(conn,=20"user-defined=20OAuth=20flow=20failed");=0A= +=09=09return=20status;=0A+=09}=0A+=09else=20if=20(status=20=3D=3D=20= PGRES_POLLING_OK)=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20We=20already=20= have=20a=20token,=20so=20copy=20it=20into=20the=20conn.=20(We=20can't=20= hold=0A+=09=09=20*=20onto=20the=20original=20string,=20since=20it=20may=20= not=20be=20safe=20for=20us=20to=20free()=0A+=09=09=20*=20it.)=0A+=09=09=20= */=0A+=09=09if=20(!request->token)=0A+=09=09{=0A+=09=09=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09=09= "user-defined=20OAuth=20flow=20did=20not=20provide=20a=20token");=0A+=09=09= =09return=20PGRES_POLLING_FAILED;=0A+=09=09}=0A+=0A+=09=09= conn->oauth_token=20=3D=20strdup(request->token);=0A+=09=09if=20= (!conn->oauth_token)=0A+=09=09{=0A+=09=09=09= libpq_append_conn_error(conn,=20"out=20of=20memory");=0A+=09=09=09return=20= PGRES_POLLING_FAILED;=0A+=09=09}=0A+=0A+=09=09return=20PGRES_POLLING_OK;=0A= +=09}=0A+=0A+=09/*=20The=20hook=20wants=20the=20client=20to=20poll=20the=20= altsock.=20Make=20sure=20it=20set=20one.=20*/=0A+=09if=20(conn->altsock=20= =3D=3D=20PGINVALID_SOCKET)=0A+=09{=0A+=09=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09"user-defined=20= OAuth=20flow=20did=20not=20provide=20a=20socket=20for=20polling");=0A+=09= =09return=20PGRES_POLLING_FAILED;=0A+=09}=0A+=0A+=09return=20status;=0A= +}=0A+=0A+/*=0A+=20*=20Cleanup=20callback=20for=20the=20async=20user=20= flow.=20Delegates=20most=20of=20its=20job=20to=20the=0A+=20*=20= user-provided=20cleanup=20implementation,=20then=20disconnects=20the=20= altsock.=0A+=20*/=0A+static=20void=0A+cleanup_user_oauth_flow(PGconn=20= *conn)=0A+{=0A+=09fe_oauth_state=20*state=20=3D=20conn->sasl_state;=0A+=09= PGoauthBearerRequest=20*request=20=3D=20state->async_ctx;=0A+=0A+=09= Assert(request);=0A+=0A+=09if=20(request->cleanup)=0A+=09=09= request->cleanup(conn,=20request);=0A+=09conn->altsock=20=3D=20= PGINVALID_SOCKET;=0A+=0A+=09free(request);=0A+=09state->async_ctx=20=3D=20= NULL;=0A+}=0A+=0A+/*=0A+=20*=20Chooses=20an=20OAuth=20client=20flow=20= for=20the=20connection,=20which=20will=20retrieve=20a=20Bearer=0A+=20*=20= token=20for=20presentation=20to=20the=20server.=0A+=20*=0A+=20*=20If=20= the=20application=20has=20registered=20a=20custom=20flow=20handler=20= using=0A+=20*=20PQAUTHDATA_OAUTH_BEARER_TOKEN,=20it=20may=20either=20= return=20a=20token=20immediately=20(e.g.=0A+=20*=20if=20it=20has=20one=20= cached=20for=20immediate=20use),=20or=20set=20up=20for=20a=20series=20of=0A= +=20*=20asynchronous=20callbacks=20which=20will=20be=20managed=20by=20= run_user_oauth_flow().=0A+=20*=0A+=20*=20If=20the=20default=20handler=20= is=20used=20instead,=20a=20Device=20Authorization=20flow=20is=20used=0A+=20= *=20for=20the=20connection=20if=20support=20has=20been=20compiled=20in.=20= (See=0A+=20*=20fe-auth-oauth-curl.c=20for=20implementation=20details.)=0A= +=20*=0A+=20*=20If=20neither=20a=20custom=20handler=20nor=20the=20= builtin=20flow=20is=20available,=20the=20connection=0A+=20*=20fails=20= here.=0A+=20*/=0A+static=20bool=0A+setup_token_request(PGconn=20*conn,=20= fe_oauth_state=20*state)=0A+{=0A+=09int=09=09=09res;=0A+=09= PGoauthBearerRequest=20request=20=3D=20{=0A+=09=09.openid_configuration=20= =3D=20conn->oauth_discovery_uri,=0A+=09=09.scope=20=3D=20= conn->oauth_scope,=0A+=09};=0A+=0A+=09= Assert(request.openid_configuration);=0A+=0A+=09/*=20The=20client=20may=20= have=20overridden=20the=20OAuth=20flow.=20*/=0A+=09res=20=3D=20= PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN,=20conn,=20&request);=0A+=09= if=20(res=20>=200)=0A+=09{=0A+=09=09PGoauthBearerRequest=20= *request_copy;=0A+=0A+=09=09if=20(request.token)=0A+=09=09{=0A+=09=09=09= /*=0A+=09=09=09=20*=20We=20already=20have=20a=20token,=20so=20copy=20it=20= into=20the=20conn.=20(We=20can't=0A+=09=09=09=20*=20hold=20onto=20the=20= original=20string,=20since=20it=20may=20not=20be=20safe=20for=20us=0A+=09= =09=09=20*=20to=20free()=20it.)=0A+=09=09=09=20*/=0A+=09=09=09= conn->oauth_token=20=3D=20strdup(request.token);=0A+=09=09=09if=20= (!conn->oauth_token)=0A+=09=09=09{=0A+=09=09=09=09= libpq_append_conn_error(conn,=20"out=20of=20memory");=0A+=09=09=09=09= goto=20fail;=0A+=09=09=09}=0A+=0A+=09=09=09/*=20short-circuit=20*/=0A+=09= =09=09if=20(request.cleanup)=0A+=09=09=09=09request.cleanup(conn,=20= &request);=0A+=09=09=09return=20true;=0A+=09=09}=0A+=0A+=09=09= request_copy=20=3D=20malloc(sizeof(*request_copy));=0A+=09=09if=20= (!request_copy)=0A+=09=09{=0A+=09=09=09libpq_append_conn_error(conn,=20= "out=20of=20memory");=0A+=09=09=09goto=20fail;=0A+=09=09}=0A+=0A+=09=09= memcpy(request_copy,=20&request,=20sizeof(request));=0A+=0A+=09=09= conn->async_auth=20=3D=20run_user_oauth_flow;=0A+=09=09= conn->cleanup_async_auth=20=3D=20cleanup_user_oauth_flow;=0A+=09=09= state->async_ctx=20=3D=20request_copy;=0A+=09}=0A+=09else=20if=20(res=20= <=200)=0A+=09{=0A+=09=09libpq_append_conn_error(conn,=20"user-defined=20= OAuth=20flow=20failed");=0A+=09=09goto=20fail;=0A+=09}=0A+=09else=0A+=09= {=0A+#if=20USE_LIBCURL=0A+=09=09/*=20Hand=20off=20to=20our=20built-in=20= OAuth=20flow.=20*/=0A+=09=09conn->async_auth=20=3D=20= pg_fe_run_oauth_flow;=0A+=09=09conn->cleanup_async_auth=20=3D=20= pg_fe_cleanup_oauth_flow;=0A+=0A+#else=0A+=09=09= libpq_append_conn_error(conn,=20"no=20custom=20OAuth=20flows=20are=20= available,=20and=20libpq=20was=20not=20built=20with=20libcurl=20= support");=0A+=09=09goto=20fail;=0A+=0A+#endif=0A+=09}=0A+=0A+=09return=20= true;=0A+=0A+fail:=0A+=09if=20(request.cleanup)=0A+=09=09= request.cleanup(conn,=20&request);=0A+=09return=20false;=0A+}=0A+=0A+/*=0A= +=20*=20Fill=20in=20our=20issuer=20identifier=20and=20discovery=20URI,=20= if=20possible,=20using=20the=0A+=20*=20connection=20parameters.=20If=20= conn->oauth_discovery_uri=20can't=20be=20populated=20in=0A+=20*=20this=20= function,=20it=20will=20be=20requested=20from=20the=20server.=0A+=20*/=0A= +static=20bool=0A+setup_oauth_parameters(PGconn=20*conn)=0A+{=0A+=09= /*---=0A+=09=20*=20To=20talk=20to=20a=20server,=20we=20require=20the=20= user=20to=20provide=20issuer=20and=20client=0A+=09=20*=20identifiers.=0A= +=09=20*=0A+=09=20*=20While=20it's=20possible=20for=20an=20OAuth=20= client=20to=20support=20multiple=20issuers,=20it=0A+=09=20*=20requires=20= additional=20effort=20to=20make=20sure=20the=20flows=20in=20use=20are=20= safe=20--=20to=0A+=09=20*=20quote=20RFC=209207,=0A+=09=20*=0A+=09=20*=20=20= =20=20=20OAuth=20clients=20that=20interact=20with=20only=20one=20= authorization=20server=20are=0A+=09=20*=20=20=20=20=20not=20vulnerable=20= to=20mix-up=20attacks.=20However,=20when=20such=20clients=20decide=0A+=09= =20*=20=20=20=20=20to=20add=20support=20for=20a=20second=20authorization=20= server=20in=20the=20future,=20they=0A+=09=20*=20=20=20=20=20become=20= vulnerable=20and=20need=20to=20apply=20countermeasures=20to=20mix-up=0A+=09= =20*=20=20=20=20=20attacks.=0A+=09=20*=0A+=09=20*=20For=20now,=20we=20= allow=20only=20one.=0A+=09=20*/=0A+=09if=20(!conn->oauth_issuer=20||=20= !conn->oauth_client_id)=0A+=09{=0A+=09=09libpq_append_conn_error(conn,=0A= +=09=09=09=09=09=09=09=09"server=20requires=20OAuth=20authentication,=20= but=20oauth_issuer=20and=20oauth_client_id=20are=20not=20both=20set");=0A= +=09=09return=20false;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20oauth_issuer=20= is=20interpreted=20differently=20if=20it's=20a=20well-known=20discovery=0A= +=09=20*=20URI=20rather=20than=20just=20an=20issuer=20identifier.=0A+=09=20= */=0A+=09if=20(strstr(conn->oauth_issuer,=20WK_PREFIX)=20!=3D=20NULL)=0A= +=09{=0A+=09=09/*=0A+=09=09=20*=20Convert=20the=20URI=20back=20to=20an=20= issuer=20identifier.=20(This=20also=20performs=0A+=09=09=20*=20= validation=20of=20the=20URI=20format.)=0A+=09=09=20*/=0A+=09=09= conn->oauth_issuer_id=20=3D=20issuer_from_well_known_uri(conn,=0A+=09=09=09= =09=09=09=09=09=09=09=09=09=09=09=20=20=20conn->oauth_issuer);=0A+=09=09= if=20(!conn->oauth_issuer_id)=0A+=09=09=09return=20false;=09=09/*=20= error=20message=20already=20set=20*/=0A+=0A+=09=09= conn->oauth_discovery_uri=20=3D=20strdup(conn->oauth_issuer);=0A+=09=09= if=20(!conn->oauth_discovery_uri)=0A+=09=09{=0A+=09=09=09= libpq_append_conn_error(conn,=20"out=20of=20memory");=0A+=09=09=09return=20= false;=0A+=09=09}=0A+=09}=0A+=09else=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20= Treat=20oauth_issuer=20as=20an=20issuer=20identifier.=20We'll=20ask=20= the=20server=0A+=09=09=20*=20for=20the=20discovery=20URI.=0A+=09=09=20*/=0A= +=09=09conn->oauth_issuer_id=20=3D=20strdup(conn->oauth_issuer);=0A+=09=09= if=20(!conn->oauth_issuer_id)=0A+=09=09{=0A+=09=09=09= libpq_append_conn_error(conn,=20"out=20of=20memory");=0A+=09=09=09return=20= false;=0A+=09=09}=0A+=09}=0A+=0A+=09return=20true;=0A+}=0A+=0A+/*=0A+=20= *=20Implements=20the=20OAUTHBEARER=20SASL=20exchange=20(RFC=207628,=20= Sec.=203.2).=0A+=20*=0A+=20*=20If=20the=20necessary=20OAuth=20parameters=20= are=20set=20up=20on=20the=20connection,=20this=20will=20run=0A+=20*=20= the=20client=20flow=20asynchronously=20and=20present=20the=20resulting=20= token=20to=20the=20server.=0A+=20*=20Otherwise,=20an=20empty=20discovery=20= response=20will=20be=20sent=20and=20any=20parameters=20sent=0A+=20*=20= back=20by=20the=20server=20will=20be=20stored=20for=20a=20second=20= attempt.=0A+=20*=0A+=20*=20For=20a=20full=20description=20of=20the=20= API,=20see=20libpq/sasl.h.=0A+=20*/=0A+static=20SASLStatus=0A= +oauth_exchange(void=20*opaq,=20bool=20final,=0A+=09=09=09=20=20=20char=20= *input,=20int=20inputlen,=0A+=09=09=09=20=20=20char=20**output,=20int=20= *outputlen)=0A+{=0A+=09fe_oauth_state=20*state=20=3D=20opaq;=0A+=09= PGconn=09=20=20=20*conn=20=3D=20state->conn;=0A+=09bool=09=09discover=20= =3D=20false;=0A+=0A+=09*output=20=3D=20NULL;=0A+=09*outputlen=20=3D=200;=0A= +=0A+=09switch=20(state->step)=0A+=09{=0A+=09=09case=20FE_OAUTH_INIT:=0A= +=09=09=09/*=20We=20begin=20in=20the=20initial=20response=20phase.=20*/=0A= +=09=09=09Assert(inputlen=20=3D=3D=20-1);=0A+=0A+=09=09=09if=20= (!setup_oauth_parameters(conn))=0A+=09=09=09=09return=20SASL_FAILED;=0A+=0A= +=09=09=09if=20(conn->oauth_token)=0A+=09=09=09{=0A+=09=09=09=09/*=0A+=09= =09=09=09=20*=20A=20previous=20connection=20already=20fetched=20the=20= token;=20we'll=20use=0A+=09=09=09=09=20*=20it=20below.=0A+=09=09=09=09=20= */=0A+=09=09=09}=0A+=09=09=09else=20if=20(conn->oauth_discovery_uri)=0A+=09= =09=09{=0A+=09=09=09=09/*=0A+=09=09=09=09=20*=20We=20don't=20have=20a=20= token,=20but=20we=20have=20a=20discovery=20URI=20already=0A+=09=09=09=09=20= *=20stored.=20Decide=20whether=20we're=20using=20a=20user-provided=20= OAuth=0A+=09=09=09=09=20*=20flow=20or=20the=20one=20we=20have=20built=20= in.=0A+=09=09=09=09=20*/=0A+=09=09=09=09if=20(!setup_token_request(conn,=20= state))=0A+=09=09=09=09=09return=20SASL_FAILED;=0A+=0A+=09=09=09=09if=20= (conn->oauth_token)=0A+=09=09=09=09{=0A+=09=09=09=09=09/*=0A+=09=09=09=09= =09=20*=20A=20really=20smart=20user=20implementation=20may=20have=20= already=0A+=09=09=09=09=09=20*=20given=20us=20the=20token=20(e.g.=20if=20= there=20was=20an=20unexpired=20copy=0A+=09=09=09=09=09=20*=20already=20= cached),=20and=20we=20can=20use=20it=20immediately.=0A+=09=09=09=09=09=20= */=0A+=09=09=09=09}=0A+=09=09=09=09else=0A+=09=09=09=09{=0A+=09=09=09=09=09= /*=0A+=09=09=09=09=09=20*=20Otherwise,=20we'll=20have=20to=20hand=20the=20= connection=20over=20to=0A+=09=09=09=09=09=20*=20our=20OAuth=20= implementation.=0A+=09=09=09=09=09=20*=0A+=09=09=09=09=09=20*=20This=20= could=20take=20a=20while,=20since=20it=20generally=20involves=20a=0A+=09=09= =09=09=09=20*=20user=20in=20the=20loop.=20To=20avoid=20consuming=20the=20= server's=0A+=09=09=09=09=09=20*=20authentication=20timeout,=20we'll=20= continue=20this=20handshake=0A+=09=09=09=09=09=20*=20to=20the=20end,=20= so=20that=20the=20server=20can=20close=20its=20side=20of=0A+=09=09=09=09=09= =20*=20the=20connection.=20We'll=20open=20a=20second=20connection=20= later=0A+=09=09=09=09=09=20*=20once=20we've=20retrieved=20a=20token.=0A+=09= =09=09=09=09=20*/=0A+=09=09=09=09=09discover=20=3D=20true;=0A+=09=09=09=09= }=0A+=09=09=09}=0A+=09=09=09else=0A+=09=09=09{=0A+=09=09=09=09/*=0A+=09=09= =09=09=20*=20If=20we=20don't=20have=20a=20token,=20and=20we=20don't=20= have=20a=20discovery=20URI=0A+=09=09=09=09=20*=20to=20be=20able=20to=20= request=20a=20token,=20we=20ask=20the=20server=20for=20one=0A+=09=09=09=09= =20*=20explicitly.=0A+=09=09=09=09=20*/=0A+=09=09=09=09discover=20=3D=20= true;=0A+=09=09=09}=0A+=0A+=09=09=09/*=0A+=09=09=09=20*=20Generate=20an=20= initial=20response.=20This=20either=20contains=20a=20token,=20if=0A+=09=09= =09=20*=20we=20have=20one,=20or=20an=20empty=20discovery=20response=20= which=20is=20doomed=20to=0A+=09=09=09=20*=20fail.=0A+=09=09=09=20*/=0A+=09= =09=09*output=20=3D=20client_initial_response(conn,=20discover);=0A+=09=09= =09if=20(!*output)=0A+=09=09=09=09return=20SASL_FAILED;=0A+=0A+=09=09=09= *outputlen=20=3D=20strlen(*output);=0A+=09=09=09state->step=20=3D=20= FE_OAUTH_BEARER_SENT;=0A+=0A+=09=09=09if=20(conn->oauth_token)=0A+=09=09=09= {=0A+=09=09=09=09/*=0A+=09=09=09=09=20*=20For=20the=20purposes=20of=20= require_auth,=20our=20side=20of=0A+=09=09=09=09=20*=20authentication=20= is=20done=20at=20this=20point;=20the=20server=20will=0A+=09=09=09=09=20*=20= either=20accept=20the=20connection=20or=20send=20an=20error.=20Unlike=0A= +=09=09=09=09=20*=20SCRAM,=20there=20is=20no=20additional=20server=20= data=20to=20check=20upon=0A+=09=09=09=09=20*=20success.=0A+=09=09=09=09=20= */=0A+=09=09=09=09conn->client_finished_auth=20=3D=20true;=0A+=09=09=09}=0A= +=0A+=09=09=09return=20SASL_CONTINUE;=0A+=0A+=09=09case=20= FE_OAUTH_BEARER_SENT:=0A+=09=09=09if=20(final)=0A+=09=09=09{=0A+=09=09=09= =09/*=0A+=09=09=09=09=20*=20OAUTHBEARER=20does=20not=20make=20use=20of=20= additional=20data=20with=20a=0A+=09=09=09=09=20*=20successful=20SASL=20= exchange,=20so=20we=20shouldn't=20get=20an=0A+=09=09=09=09=20*=20= AuthenticationSASLFinal=20message.=0A+=09=09=09=09=20*/=0A+=09=09=09=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09=09=09"server=20= sent=20unexpected=20additional=20OAuth=20data");=0A+=09=09=09=09return=20= SASL_FAILED;=0A+=09=09=09}=0A+=0A+=09=09=09/*=0A+=09=09=09=20*=20An=20= error=20message=20was=20sent=20by=20the=20server.=20Respond=20with=20the=0A= +=09=09=09=20*=20required=20dummy=20message=20(RFC=207628,=20sec.=20= 3.2.3).=0A+=09=09=09=20*/=0A+=09=09=09*output=20=3D=20strdup(kvsep);=0A+=09= =09=09if=20(unlikely(!*output))=0A+=09=09=09{=0A+=09=09=09=09= libpq_append_conn_error(conn,=20"out=20of=20memory");=0A+=09=09=09=09= return=20SASL_FAILED;=0A+=09=09=09}=0A+=09=09=09*outputlen=20=3D=20= strlen(*output);=09/*=20=3D=3D=201=20*/=0A+=0A+=09=09=09/*=20Grab=20the=20= settings=20from=20discovery.=20*/=0A+=09=09=09if=20= (!handle_oauth_sasl_error(conn,=20input,=20inputlen))=0A+=09=09=09=09= return=20SASL_FAILED;=0A+=0A+=09=09=09if=20(conn->oauth_token)=0A+=09=09=09= {=0A+=09=09=09=09/*=0A+=09=09=09=09=20*=20The=20server=20rejected=20our=20= token.=20Continue=20onwards=20towards=20the=0A+=09=09=09=09=20*=20= expected=20FATAL=20message,=20but=20mark=20our=20state=20to=20catch=20= any=0A+=09=09=09=09=20*=20unexpected=20"success"=20from=20the=20server.=0A= +=09=09=09=09=20*/=0A+=09=09=09=09state->step=20=3D=20= FE_OAUTH_SERVER_ERROR;=0A+=09=09=09=09return=20SASL_CONTINUE;=0A+=09=09=09= }=0A+=0A+=09=09=09if=20(!conn->async_auth)=0A+=09=09=09{=0A+=09=09=09=09= /*=0A+=09=09=09=09=20*=20No=20OAuth=20flow=20is=20set=20up=20yet.=20Did=20= we=20get=20enough=20information=0A+=09=09=09=09=20*=20from=20the=20= server=20to=20create=20one?=0A+=09=09=09=09=20*/=0A+=09=09=09=09if=20= (!conn->oauth_discovery_uri)=0A+=09=09=09=09{=0A+=09=09=09=09=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09=09=09=09= "server=20requires=20OAuth=20authentication,=20but=20no=20discovery=20= metadata=20was=20provided");=0A+=09=09=09=09=09return=20SASL_FAILED;=0A+=09= =09=09=09}=0A+=0A+=09=09=09=09/*=20Yes.=20Set=20up=20the=20flow=20now.=20= */=0A+=09=09=09=09if=20(!setup_token_request(conn,=20state))=0A+=09=09=09= =09=09return=20SASL_FAILED;=0A+=0A+=09=09=09=09if=20(conn->oauth_token)=0A= +=09=09=09=09{=0A+=09=09=09=09=09/*=0A+=09=09=09=09=09=20*=20A=20token=20= was=20available=20in=20a=20custom=20flow's=20cache.=20Skip=0A+=09=09=09=09= =09=20*=20the=20asynchronous=20processing.=0A+=09=09=09=09=09=20*/=0A+=09= =09=09=09=09goto=20reconnect;=0A+=09=09=09=09}=0A+=09=09=09}=0A+=0A+=09=09= =09/*=0A+=09=09=09=20*=20Time=20to=20retrieve=20a=20token.=20This=20= involves=20a=20number=20of=20HTTP=0A+=09=09=09=20*=20connections=20and=20= timed=20waits,=20so=20we=20escape=20the=20synchronous=20auth=0A+=09=09=09= =20*=20processing=20and=20tell=20PQconnectPoll=20to=20transfer=20control=20= to=20our=0A+=09=09=09=20*=20async=20implementation.=0A+=09=09=09=20*/=0A= +=09=09=09Assert(conn->async_auth);=09/*=20should=20have=20been=20set=20= already=20*/=0A+=09=09=09state->step=20=3D=20FE_OAUTH_REQUESTING_TOKEN;=0A= +=09=09=09return=20SASL_ASYNC;=0A+=0A+=09=09case=20= FE_OAUTH_REQUESTING_TOKEN:=0A+=0A+=09=09=09/*=0A+=09=09=09=20*=20We've=20= returned=20successfully=20from=20token=20retrieval.=20Double-check=0A+=09= =09=09=20*=20that=20we=20have=20what=20we=20need=20for=20the=20next=20= connection.=0A+=09=09=09=20*/=0A+=09=09=09if=20(!conn->oauth_token)=0A+=09= =09=09{=0A+=09=09=09=09Assert(false);=09/*=20should=20have=20failed=20= before=20this=20point!=20*/=0A+=09=09=09=09libpq_append_conn_error(conn,=0A= +=09=09=09=09=09=09=09=09=09=09"internal=20error:=20OAuth=20flow=20did=20= not=20set=20a=20token");=0A+=09=09=09=09return=20SASL_FAILED;=0A+=09=09=09= }=0A+=0A+=09=09=09goto=20reconnect;=0A+=0A+=09=09case=20= FE_OAUTH_SERVER_ERROR:=0A+=0A+=09=09=09/*=0A+=09=09=09=20*=20After=20an=20= error,=20the=20server=20should=20send=20an=20error=20response=20to=0A+=09= =09=09=20*=20fail=20the=20SASL=20handshake,=20which=20is=20handled=20in=20= higher=20layers.=0A+=09=09=09=20*=0A+=09=09=09=20*=20If=20we=20get=20= here,=20the=20server=20either=20sent=20*another*=20challenge=0A+=09=09=09= =20*=20which=20isn't=20defined=20in=20the=20RFC,=20or=20completed=20the=20= handshake=0A+=09=09=09=20*=20successfully=20after=20telling=20us=20it=20= was=20going=20to=20fail.=20Neither=20is=0A+=09=09=09=20*=20acceptable.=0A= +=09=09=09=20*/=0A+=09=09=09libpq_append_conn_error(conn,=0A+=09=09=09=09= =09=09=09=09=09"server=20sent=20additional=20OAuth=20data=20after=20= error");=0A+=09=09=09return=20SASL_FAILED;=0A+=0A+=09=09default:=0A+=09=09= =09libpq_append_conn_error(conn,=20"invalid=20OAuth=20exchange=20= state");=0A+=09=09=09break;=0A+=09}=0A+=0A+=09Assert(false);=09=09=09=09= /*=20should=20never=20get=20here=20*/=0A+=09return=20SASL_FAILED;=0A+=0A= +reconnect:=0A+=0A+=09/*=0A+=09=20*=20Despite=20being=20a=20failure=20= from=20the=20point=20of=20view=20of=20SASL,=20we=20have=20enough=0A+=09=20= *=20information=20to=20restart=20with=20a=20new=20connection.=0A+=09=20= */=0A+=09libpq_append_conn_error(conn,=20"retrying=20connection=20with=20= new=20bearer=20token");=0A+=09conn->oauth_want_retry=20=3D=20true;=0A+=09= return=20SASL_FAILED;=0A+}=0A+=0A+static=20bool=0A= +oauth_channel_bound(void=20*opaq)=0A+{=0A+=09/*=20This=20mechanism=20= does=20not=20support=20channel=20binding.=20*/=0A+=09return=20false;=0A= +}=0A+=0A+/*=0A+=20*=20Fully=20clears=20out=20any=20stored=20OAuth=20= token.=20This=20is=20done=20proactively=20upon=0A+=20*=20successful=20= connection=20as=20well=20as=20during=20pqClosePGconn().=0A+=20*/=0A+void=0A= +pqClearOAuthToken(PGconn=20*conn)=0A+{=0A+=09if=20(!conn->oauth_token)=0A= +=09=09return;=0A+=0A+=09explicit_bzero(conn->oauth_token,=20= strlen(conn->oauth_token));=0A+=09free(conn->oauth_token);=0A+=09= conn->oauth_token=20=3D=20NULL;=0A+}=0A+=0A+/*=0A+=20*=20Returns=20true=20= if=20the=20PGOAUTHDEBUG=3DUNSAFE=20flag=20is=20set=20in=20the=20= environment.=0A+=20*/=0A+bool=0A+oauth_unsafe_debugging_enabled(void)=0A= +{=0A+=09const=20char=20*env=20=3D=20getenv("PGOAUTHDEBUG");=0A+=0A+=09= return=20(env=20&&=20strcmp(env,=20"UNSAFE")=20=3D=3D=200);=0A+}=0Adiff=20= --git=20a/src/interfaces/libpq/fe-auth-oauth.h=20= b/src/interfaces/libpq/fe-auth-oauth.h=0Anew=20file=20mode=20100644=0A= index=2000000000000..32598721686=0A---=20/dev/null=0A+++=20= b/src/interfaces/libpq/fe-auth-oauth.h=0A@@=20-0,0=20+1,45=20@@=0A= +/*-----------------------------------------------------------------------= --=0A+=20*=0A+=20*=20fe-auth-oauth.h=0A+=20*=0A+=20*=09=20=20Definitions=20= for=20OAuth=20authentication=20implementations=0A+=20*=0A+=20*=20= Portions=20Copyright=20(c)=202024,=20PostgreSQL=20Global=20Development=20= Group=0A+=20*=0A+=20*=20src/interfaces/libpq/fe-auth-oauth.h=0A+=20*=0A+=20= *-------------------------------------------------------------------------= =0A+=20*/=0A+=0A+#ifndef=20FE_AUTH_OAUTH_H=0A+#define=20FE_AUTH_OAUTH_H=0A= +=0A+#include=20"libpq-fe.h"=0A+#include=20"libpq-int.h"=0A+=0A+=0A+enum=20= fe_oauth_step=0A+{=0A+=09FE_OAUTH_INIT,=0A+=09FE_OAUTH_BEARER_SENT,=0A+=09= FE_OAUTH_REQUESTING_TOKEN,=0A+=09FE_OAUTH_SERVER_ERROR,=0A+};=0A+=0A= +typedef=20struct=0A+{=0A+=09enum=20fe_oauth_step=20step;=0A+=0A+=09= PGconn=09=20=20=20*conn;=0A+=09void=09=20=20=20*async_ctx;=0A+}=20= fe_oauth_state;=0A+=0A+extern=20PostgresPollingStatusType=20= pg_fe_run_oauth_flow(PGconn=20*conn);=0A+extern=20void=20= pg_fe_cleanup_oauth_flow(PGconn=20*conn);=0A+extern=20void=20= pqClearOAuthToken(PGconn=20*conn);=0A+extern=20bool=20= oauth_unsafe_debugging_enabled(void);=0A+=0A+/*=20Mechanisms=20in=20= fe-auth-oauth.c=20*/=0A+extern=20const=20pg_fe_sasl_mech=20= pg_oauth_mech;=0A+=0A+#endif=09=09=09=09=09=09=09/*=20FE_AUTH_OAUTH_H=20= */=0Adiff=20--git=20a/src/interfaces/libpq/fe-auth.c=20= b/src/interfaces/libpq/fe-auth.c=0Aindex=20761ee8f88f7..ec7a9236044=20= 100644=0A---=20a/src/interfaces/libpq/fe-auth.c=0A+++=20= b/src/interfaces/libpq/fe-auth.c=0A@@=20-40,9=20+40,11=20@@=0A=20#endif=0A= =20=0A=20#include=20"common/md5.h"=0A+#include=20"common/oauth-common.h"=0A= =20#include=20"common/scram-common.h"=0A=20#include=20"fe-auth.h"=0A=20= #include=20"fe-auth-sasl.h"=0A+#include=20"fe-auth-oauth.h"=0A=20= #include=20"libpq-fe.h"=0A=20=0A=20#ifdef=20ENABLE_GSS=0A@@=20-535,6=20= +537,13=20@@=20pg_SASL_init(PGconn=20*conn,=20int=20payloadlen,=20bool=20= *async)=0A=20=09=09=09conn->sasl=20=3D=20&pg_scram_mech;=0A=20=09=09=09= conn->password_needed=20=3D=20true;=0A=20=09=09}=0A+=09=09else=20if=20= (strcmp(mechanism_buf.data,=20OAUTHBEARER_NAME)=20=3D=3D=200=20&&=0A+=09=09= =09=09=20!selected_mechanism)=0A+=09=09{=0A+=09=09=09selected_mechanism=20= =3D=20OAUTHBEARER_NAME;=0A+=09=09=09conn->sasl=20=3D=20&pg_oauth_mech;=0A= +=09=09=09conn->password_needed=20=3D=20false;=0A+=09=09}=0A=20=09}=0A=20= =0A=20=09if=20(!selected_mechanism)=0A@@=20-559,13=20+568,6=20@@=20= pg_SASL_init(PGconn=20*conn,=20int=20payloadlen,=20bool=20*async)=0A=20=0A= =20=09=09if=20(!allowed)=0A=20=09=09{=0A-=09=09=09/*=0A-=09=09=09=20*=20= TODO:=20this=20is=20dead=20code=20until=20a=20second=20SASL=20mechanism=20= is=20added;=0A-=09=09=09=20*=20the=20connection=20can't=20have=20= proceeded=20past=20check_expected_areq()=0A-=09=09=09=20*=20if=20no=20= SASL=20methods=20are=20allowed.=0A-=09=09=09=20*/=0A-=09=09=09= Assert(false);=0A-=0A=20=09=09=09libpq_append_conn_error(conn,=20= "authentication=20method=20requirement=20\"%s\"=20failed:=20server=20= requested=20%s=20authentication",=0A=20=09=09=09=09=09=09=09=09=09= conn->require_auth,=20selected_mechanism);=0A=20=09=09=09goto=20error;=0A= @@=20-1580,3=20+1582,23=20@@=20PQchangePassword(PGconn=20*conn,=20const=20= char=20*user,=20const=20char=20*passwd)=0A=20=09=09}=0A=20=09}=0A=20}=0A= +=0A+PQauthDataHook_type=20PQauthDataHook=20=3D=20PQdefaultAuthDataHook;=0A= +=0A+PQauthDataHook_type=0A+PQgetAuthDataHook(void)=0A+{=0A+=09return=20= PQauthDataHook;=0A+}=0A+=0A+void=0A= +PQsetAuthDataHook(PQauthDataHook_type=20hook)=0A+{=0A+=09PQauthDataHook=20= =3D=20hook=20?=20hook=20:=20PQdefaultAuthDataHook;=0A+}=0A+=0A+int=0A= +PQdefaultAuthDataHook(PGauthData=20type,=20PGconn=20*conn,=20void=20= *data)=0A+{=0A+=09return=200;=09=09=09=09=09/*=20handle=20nothing=20*/=0A= +}=0Adiff=20--git=20a/src/interfaces/libpq/fe-auth.h=20= b/src/interfaces/libpq/fe-auth.h=0Aindex=201d4991f8996..de98e0d20c4=20= 100644=0A---=20a/src/interfaces/libpq/fe-auth.h=0A+++=20= b/src/interfaces/libpq/fe-auth.h=0A@@=20-18,6=20+18,9=20@@=0A=20#include=20= "libpq-int.h"=0A=20=0A=20=0A+extern=20PQauthDataHook_type=20= PQauthDataHook;=0A+=0A+=0A=20/*=20Prototypes=20for=20functions=20in=20= fe-auth.c=20*/=0A=20extern=20int=09pg_fe_sendauth(AuthRequest=20areq,=20= int=20payloadlen,=20PGconn=20*conn,=0A=20=09=09=09=09=09=09=20=20=20bool=20= *async);=0Adiff=20--git=20a/src/interfaces/libpq/fe-connect.c=20= b/src/interfaces/libpq/fe-connect.c=0Aindex=2085d1ca2864f..d5051f5e820=20= 100644=0A---=20a/src/interfaces/libpq/fe-connect.c=0A+++=20= b/src/interfaces/libpq/fe-connect.c=0A@@=20-28,6=20+28,7=20@@=0A=20= #include=20"common/scram-common.h"=0A=20#include=20"common/string.h"=0A=20= #include=20"fe-auth.h"=0A+#include=20"fe-auth-oauth.h"=0A=20#include=20= "libpq-fe.h"=0A=20#include=20"libpq-int.h"=0A=20#include=20= "mb/pg_wchar.h"=0A@@=20-373,6=20+374,23=20@@=20static=20const=20= internalPQconninfoOption=20PQconninfoOptions[]=20=3D=20{=0A=20=09= {"scram_server_key",=20NULL,=20NULL,=20NULL,=20"SCRAM-Server-Key",=20= "D",=20SCRAM_MAX_KEY_LEN=20*=202,=0A=20=09offsetof(struct=20pg_conn,=20= scram_server_key)},=0A=20=0A+=09/*=20OAuth=20v2=20*/=0A+=09= {"oauth_issuer",=20NULL,=20NULL,=20NULL,=0A+=09=09"OAuth-Issuer",=20"",=20= 40,=0A+=09offsetof(struct=20pg_conn,=20oauth_issuer)},=0A+=0A+=09= {"oauth_client_id",=20NULL,=20NULL,=20NULL,=0A+=09=09"OAuth-Client-ID",=20= "",=2040,=0A+=09offsetof(struct=20pg_conn,=20oauth_client_id)},=0A+=0A+=09= {"oauth_client_secret",=20NULL,=20NULL,=20NULL,=0A+=09=09= "OAuth-Client-Secret",=20"",=2040,=0A+=09offsetof(struct=20pg_conn,=20= oauth_client_secret)},=0A+=0A+=09{"oauth_scope",=20NULL,=20NULL,=20NULL,=0A= +=09=09"OAuth-Scope",=20"",=2015,=0A+=09offsetof(struct=20pg_conn,=20= oauth_scope)},=0A+=0A=20=09/*=20Terminating=20entry=20---=20MUST=20BE=20= LAST=20*/=0A=20=09{NULL,=20NULL,=20NULL,=20NULL,=0A=20=09NULL,=20NULL,=20= 0}=0A@@=20-399,6=20+417,7=20@@=20static=20const=20PQEnvironmentOption=20= EnvironmentOptions[]=20=3D=0A=20static=20const=20pg_fe_sasl_mech=20= *supported_sasl_mechs[]=20=3D=0A=20{=0A=20=09&pg_scram_mech,=0A+=09= &pg_oauth_mech,=0A=20};=0A=20#define=20SASL_MECHANISM_COUNT=20= lengthof(supported_sasl_mechs)=0A=20=0A@@=20-655,6=20+674,7=20@@=20= pqDropServerData(PGconn=20*conn)=0A=20=09conn->write_failed=20=3D=20= false;=0A=20=09free(conn->write_err_msg);=0A=20=09conn->write_err_msg=20= =3D=20NULL;=0A+=09conn->oauth_want_retry=20=3D=20false;=0A=20=0A=20=09/*=0A= =20=09=20*=20Cancel=20connections=20need=20to=20retain=20their=20be_pid=20= and=20be_key=20across=0A@@=20-1144,7=20+1164,7=20@@=20static=20inline=20= void=0A=20fill_allowed_sasl_mechs(PGconn=20*conn)=0A=20{=0A=20=09/*---=0A= -=09=20*=20We=20only=20support=20one=20mechanism=20at=20the=20moment,=20= so=20rather=20than=20deal=20with=20a=0A+=09=20*=20We=20only=20support=20= two=20mechanisms=20at=20the=20moment,=20so=20rather=20than=20deal=20with=20= a=0A=20=09=20*=20linked=20list,=20conn->allowed_sasl_mechs=20is=20an=20= array=20of=20static=20length.=20We=0A=20=09=20*=20rely=20on=20the=20= compile-time=20assertion=20here=20to=20keep=20us=20honest.=0A=20=09=20*=0A= @@=20-1519,6=20+1539,10=20@@=20pqConnectOptions2(PGconn=20*conn)=0A=20=09= =09=09{=0A=20=09=09=09=09mech=20=3D=20&pg_scram_mech;=0A=20=09=09=09}=0A= +=09=09=09else=20if=20(strcmp(method,=20"oauth")=20=3D=3D=200)=0A+=09=09=09= {=0A+=09=09=09=09mech=20=3D=20&pg_oauth_mech;=0A+=09=09=09}=0A=20=0A=20=09= =09=09/*=0A=20=09=09=09=20*=20Final=20group:=20meta-options.=0A@@=20= -4111,7=20+4135,19=20@@=20keep_going:=09=09=09=09=09=09/*=20We=20will=20= come=20back=20to=20here=20until=20there=20is=0A=20=09=09=09=09= conn->inStart=20=3D=20conn->inCursor;=0A=20=0A=20=09=09=09=09if=20(res=20= !=3D=20STATUS_OK)=0A+=09=09=09=09{=0A+=09=09=09=09=09/*=0A+=09=09=09=09=09= =20*=20OAuth=20connections=20may=20perform=20two-step=20discovery,=20= where=0A+=09=09=09=09=09=20*=20the=20first=20connection=20is=20a=20= dummy.=0A+=09=09=09=09=09=20*/=0A+=09=09=09=09=09if=20(conn->sasl=20=3D=3D= =20&pg_oauth_mech=20&&=20conn->oauth_want_retry)=0A+=09=09=09=09=09{=0A+=09= =09=09=09=09=09need_new_connection=20=3D=20true;=0A+=09=09=09=09=09=09= goto=20keep_going;=0A+=09=09=09=09=09}=0A+=0A=20=09=09=09=09=09goto=20= error_return;=0A+=09=09=09=09}=0A=20=0A=20=09=09=09=09/*=0A=20=09=09=09=09= =20*=20Just=20make=20sure=20that=20any=20data=20sent=20by=20= pg_fe_sendauth=20is=0A@@=20-4390,6=20+4426,9=20@@=20keep_going:=09=09=09=09= =09=09/*=20We=20will=20come=20back=20to=20here=20until=20there=20is=0A=20= =09=09=09=09=09}=0A=20=09=09=09=09}=0A=20=0A+=09=09=09=09/*=20Don't=20= hold=20onto=20any=20OAuth=20tokens=20longer=20than=20necessary.=20*/=0A+=09= =09=09=09pqClearOAuthToken(conn);=0A+=0A=20=09=09=09=09/*=0A=20=09=09=09=09= =20*=20For=20non=20cancel=20requests=20we=20can=20release=20the=20= address=20list=0A=20=09=09=09=09=20*=20now.=20For=20cancel=20requests=20= we=20never=20actually=20resolve=0A@@=20-5002,6=20+5041,12=20@@=20= freePGconn(PGconn=20*conn)=0A=20=09free(conn->load_balance_hosts);=0A=20=09= free(conn->scram_client_key);=0A=20=09free(conn->scram_server_key);=0A+=09= free(conn->oauth_issuer);=0A+=09free(conn->oauth_issuer_id);=0A+=09= free(conn->oauth_discovery_uri);=0A+=09free(conn->oauth_client_id);=0A+=09= free(conn->oauth_client_secret);=0A+=09free(conn->oauth_scope);=0A=20=09= termPQExpBuffer(&conn->errorMessage);=0A=20=09= termPQExpBuffer(&conn->workBuffer);=0A=20=0A@@=20-5155,6=20+5200,7=20@@=20= pqClosePGconn(PGconn=20*conn)=0A=20=09conn->asyncStatus=20=3D=20= PGASYNC_IDLE;=0A=20=09conn->xactStatus=20=3D=20PQTRANS_IDLE;=0A=20=09= conn->pipelineStatus=20=3D=20PQ_PIPELINE_OFF;=0A+=09= pqClearOAuthToken(conn);=0A=20=09pqClearAsyncResult(conn);=09/*=20= deallocate=20result=20*/=0A=20=09pqClearConnErrorState(conn);=0A=20=0A= diff=20--git=20a/src/interfaces/libpq/libpq-fe.h=20= b/src/interfaces/libpq/libpq-fe.h=0Aindex=20a3491faf0c3..b7399dee58e=20= 100644=0A---=20a/src/interfaces/libpq/libpq-fe.h=0A+++=20= b/src/interfaces/libpq/libpq-fe.h=0A@@=20-59,6=20+59,8=20@@=20extern=20= "C"=0A=20/*=20Features=20added=20in=20PostgreSQL=20v18:=20*/=0A=20/*=20= Indicates=20presence=20of=20PQfullProtocolVersion=20*/=0A=20#define=20= LIBPQ_HAS_FULL_PROTOCOL_VERSION=201=0A+/*=20Indicates=20presence=20of=20= the=20PQAUTHDATA_PROMPT_OAUTH_DEVICE=20authdata=20hook=20*/=0A+#define=20= LIBPQ_HAS_PROMPT_OAUTH_DEVICE=201=0A=20=0A=20/*=0A=20=20*=20Option=20= flags=20for=20PQcopyResult=0A@@=20-186,6=20+188,13=20@@=20typedef=20enum=0A= =20=09PQ_PIPELINE_ABORTED=0A=20}=20PGpipelineStatus;=0A=20=0A+typedef=20= enum=0A+{=0A+=09PQAUTHDATA_PROMPT_OAUTH_DEVICE,=20/*=20user=20must=20= visit=20a=20device-authorization=0A+=09=09=09=09=09=09=09=09=09=20*=20= URL=20*/=0A+=09PQAUTHDATA_OAUTH_BEARER_TOKEN,=09/*=20server=20requests=20= an=20OAuth=20Bearer=20token=20*/=0A+}=20PGauthData;=0A+=0A=20/*=20PGconn=20= encapsulates=20a=20connection=20to=20the=20backend.=0A=20=20*=20The=20= contents=20of=20this=20struct=20are=20not=20supposed=20to=20be=20known=20= to=20applications.=0A=20=20*/=0A@@=20-720,10=20+729,86=20@@=20extern=20= int=09PQenv2encoding(void);=0A=20=0A=20/*=20=3D=3D=3D=20in=20fe-auth.c=20= =3D=3D=3D=20*/=0A=20=0A+typedef=20struct=20_PGpromptOAuthDevice=0A+{=0A+=09= const=20char=20*verification_uri;=09/*=20verification=20URI=20to=20visit=20= */=0A+=09const=20char=20*user_code;=09=09/*=20user=20code=20to=20enter=20= */=0A+=09const=20char=20*verification_uri_complete;=09/*=20optional=20= combination=20of=20URI=20and=0A+=09=09=09=09=09=09=09=09=09=09=09=20*=20= code,=20or=20NULL=20*/=0A+=09int=09=09=09expires_in;=09=09/*=20seconds=20= until=20user=20code=20expires=20*/=0A+}=20PGpromptOAuthDevice;=0A+=0A+/*=20= for=20PGoauthBearerRequest.async()=20*/=0A+#ifdef=20_WIN32=0A+#define=20= SOCKTYPE=20uintptr_t=09=09/*=20avoids=20depending=20on=20winsock2.h=20= for=20SOCKET=20*/=0A+#else=0A+#define=20SOCKTYPE=20int=0A+#endif=0A+=0A= +typedef=20struct=20_PGoauthBearerRequest=0A+{=0A+=09/*=20Hook=20inputs=20= (constant=20across=20all=20calls)=20*/=0A+=09const=20char=20*const=20= openid_configuration;=20/*=20OIDC=20discovery=20URI=20*/=0A+=09const=20= char=20*const=20scope;=09/*=20required=20scope(s),=20or=20NULL=20*/=0A+=0A= +=09/*=20Hook=20outputs=20*/=0A+=0A+=09/*---------=0A+=09=20*=20Callback=20= implementing=20a=20custom=20asynchronous=20OAuth=20flow.=0A+=09=20*=0A+=09= =20*=20The=20callback=20may=20return=0A+=09=20*=20-=20= PGRES_POLLING_READING/WRITING,=20to=20indicate=20that=20a=20socket=20= descriptor=0A+=09=20*=20=20=20has=20been=20stored=20in=20*altsock=20and=20= libpq=20should=20wait=20until=20it=20is=0A+=09=20*=20=20=20readable=20or=20= writable=20before=20calling=20back;=0A+=09=20*=20-=20PGRES_POLLING_OK,=20= to=20indicate=20that=20the=20flow=20is=20complete=20and=0A+=09=20*=20=20=20= request->token=20has=20been=20set;=20or=0A+=09=20*=20-=20= PGRES_POLLING_FAILED,=20to=20indicate=20that=20token=20retrieval=20has=20= failed.=0A+=09=20*=0A+=09=20*=20This=20callback=20is=20optional.=20If=20= the=20token=20can=20be=20obtained=20without=0A+=09=20*=20blocking=20= during=20the=20original=20call=20to=20the=20= PQAUTHDATA_OAUTH_BEARER_TOKEN=0A+=09=20*=20hook,=20it=20may=20be=20= returned=20directly,=20but=20one=20of=20request->async=20or=0A+=09=20*=20= request->token=20must=20be=20set=20by=20the=20hook.=0A+=09=20*/=0A+=09= PostgresPollingStatusType=20(*async)=20(PGconn=20*conn,=0A+=09=09=09=09=09= =09=09=09=09=09struct=20_PGoauthBearerRequest=20*request,=0A+=09=09=09=09= =09=09=09=09=09=09SOCKTYPE=20*=20altsock);=0A+=0A+=09/*=0A+=09=20*=20= Callback=20to=20clean=20up=20custom=20allocations.=20A=20hook=20= implementation=20may=20use=0A+=09=20*=20this=20to=20free=20= request->token=20and=20any=20resources=20in=20request->user.=0A+=09=20*=0A= +=09=20*=20This=20is=20technically=20optional,=20but=20highly=20= recommended,=20because=20there=20is=0A+=09=20*=20no=20other=20indication=20= as=20to=20when=20it=20is=20safe=20to=20free=20the=20token.=0A+=09=20*/=0A= +=09void=09=09(*cleanup)=20(PGconn=20*conn,=20struct=20= _PGoauthBearerRequest=20*request);=0A+=0A+=09/*=0A+=09=20*=20The=20hook=20= should=20set=20this=20to=20the=20Bearer=20token=20contents=20for=20the=0A= +=09=20*=20connection,=20once=20the=20flow=20is=20completed.=20=20The=20= token=20contents=20must=20remain=0A+=09=20*=20available=20to=20libpq=20= until=20the=20hook's=20cleanup=20callback=20is=20called.=0A+=09=20*/=0A+=09= char=09=20=20=20*token;=0A+=0A+=09/*=0A+=09=20*=20Hook-defined=20data.=20= libpq=20will=20not=20modify=20this=20pointer=20across=20calls=20to=0A+=09= =20*=20the=20async=20callback,=20so=20it=20can=20be=20used=20to=20keep=20= track=20of=0A+=09=20*=20application-specific=20state.=20Resources=20= allocated=20here=20should=20be=20freed=20by=0A+=09=20*=20the=20cleanup=20= callback.=0A+=09=20*/=0A+=09void=09=20=20=20*user;=0A+}=20= PGoauthBearerRequest;=0A+=0A+#undef=20SOCKTYPE=0A+=0A=20extern=20char=20= *PQencryptPassword(const=20char=20*passwd,=20const=20char=20*user);=0A=20= extern=20char=20*PQencryptPasswordConn(PGconn=20*conn,=20const=20char=20= *passwd,=20const=20char=20*user,=20const=20char=20*algorithm);=0A=20= extern=20PGresult=20*PQchangePassword(PGconn=20*conn,=20const=20char=20= *user,=20const=20char=20*passwd);=0A=20=0A+typedef=20int=20= (*PQauthDataHook_type)=20(PGauthData=20type,=20PGconn=20*conn,=20void=20= *data);=0A+extern=20void=20PQsetAuthDataHook(PQauthDataHook_type=20= hook);=0A+extern=20PQauthDataHook_type=20PQgetAuthDataHook(void);=0A= +extern=20int=09PQdefaultAuthDataHook(PGauthData=20type,=20PGconn=20= *conn,=20void=20*data);=0A+=0A=20/*=20=3D=3D=3D=20in=20encnames.c=20=3D=3D= =3D=20*/=0A=20=0A=20extern=20int=09pg_char_to_encoding(const=20char=20= *name);=0Adiff=20--git=20a/src/interfaces/libpq/libpq-int.h=20= b/src/interfaces/libpq/libpq-int.h=0Aindex=202546f9f8a50..f36f7f19d58=20= 100644=0A---=20a/src/interfaces/libpq/libpq-int.h=0A+++=20= b/src/interfaces/libpq/libpq-int.h=0A@@=20-437,6=20+437,17=20@@=20struct=20= pg_conn=0A=20=09=09=09=09=09=09=09=09=20*=20cancel=20request,=20instead=20= of=20being=20a=20normal=0A=20=09=09=09=09=09=09=09=09=20*=20connection=20= that's=20used=20for=20queries=20*/=0A=20=0A+=09/*=20OAuth=20v2=20*/=0A+=09= char=09=20=20=20*oauth_issuer;=09/*=20token=20issuer/URL=20*/=0A+=09char=09= =20=20=20*oauth_issuer_id;=09/*=20token=20issuer=20identifier=20*/=0A+=09= char=09=20=20=20*oauth_discovery_uri;=09/*=20URI=20of=20the=20issuer's=20= discovery=0A+=09=09=09=09=09=09=09=09=09=09=20*=20document=20*/=0A+=09= char=09=20=20=20*oauth_client_id;=09/*=20client=20identifier=20*/=0A+=09= char=09=20=20=20*oauth_client_secret;=09/*=20client=20secret=20*/=0A+=09= char=09=20=20=20*oauth_scope;=09/*=20access=20token=20scope=20*/=0A+=09= char=09=20=20=20*oauth_token;=09/*=20access=20token=20*/=0A+=09bool=09=09= oauth_want_retry;=09/*=20should=20we=20retry=20on=20failure?=20*/=0A+=0A=20= =09/*=20Optional=20file=20to=20write=20trace=20info=20to=20*/=0A=20=09= FILE=09=20=20=20*Pfdebug;=0A=20=09int=09=09=09traceFlags;=0A@@=20-505,7=20= +516,7=20@@=20struct=20pg_conn=0A=20=09=09=09=09=09=09=09=09=20*=20the=20= server?=20*/=0A=20=09uint32=09=09allowed_auth_methods;=09/*=20bitmask=20= of=20acceptable=20AuthRequest=0A=20=09=09=09=09=09=09=09=09=09=09=20*=20= codes=20*/=0A-=09const=20pg_fe_sasl_mech=20*allowed_sasl_mechs[1];=09/*=20= and=20acceptable=20SASL=0A+=09const=20pg_fe_sasl_mech=20= *allowed_sasl_mechs[2];=09/*=20and=20acceptable=20SASL=0A=20=09=09=09=09=09= =09=09=09=09=09=09=09=09=20*=20mechanisms=20*/=0A=20=09bool=09=09= client_finished_auth;=09/*=20have=20we=20finished=20our=20half=20of=20= the=0A=20=09=09=09=09=09=09=09=09=09=09=20*=20authentication=20exchange?=20= */=0Adiff=20--git=20a/src/interfaces/libpq/meson.build=20= b/src/interfaces/libpq/meson.build=0Aindex=20dd64d291b3e..19f4a52a97a=20= 100644=0A---=20a/src/interfaces/libpq/meson.build=0A+++=20= b/src/interfaces/libpq/meson.build=0A@@=20-1,6=20+1,7=20@@=0A=20#=20= Copyright=20(c)=202022-2025,=20PostgreSQL=20Global=20Development=20Group=0A= =20=0A=20libpq_sources=20=3D=20files(=0A+=20=20'fe-auth-oauth.c',=0A=20=20= =20'fe-auth-scram.c',=0A=20=20=20'fe-auth.c',=0A=20=20=20'fe-cancel.c',=0A= @@=20-37,6=20+38,10=20@@=20if=20gssapi.found()=0A=20=20=20)=0A=20endif=0A= =20=0A+if=20libcurl.found()=0A+=20=20libpq_sources=20+=3D=20= files('fe-auth-oauth-curl.c')=0A+endif=0A+=0A=20export_file=20=3D=20= custom_target('libpq.exports',=0A=20=20=20kwargs:=20gen_export_kwargs,=0A= =20)=0Adiff=20--git=20a/src/makefiles/meson.build=20= b/src/makefiles/meson.build=0Aindex=20d49b2079a44..60e13d50235=20100644=0A= ---=20a/src/makefiles/meson.build=0A+++=20b/src/makefiles/meson.build=0A= @@=20-229,6=20+229,7=20@@=20pgxs_deps=20=3D=20{=0A=20=20=20'gssapi':=20= gssapi,=0A=20=20=20'icu':=20icu,=0A=20=20=20'ldap':=20ldap,=0A+=20=20= 'libcurl':=20libcurl,=0A=20=20=20'libxml':=20libxml,=0A=20=20=20= 'libxslt':=20libxslt,=0A=20=20=20'llvm':=20llvm,=0Adiff=20--git=20= a/src/test/authentication/t/001_password.pl=20= b/src/test/authentication/t/001_password.pl=0Aindex=20= 1357f806b6f..4ce22ccbdf2=20100644=0A---=20= a/src/test/authentication/t/001_password.pl=0A+++=20= b/src/test/authentication/t/001_password.pl=0A@@=20-404,11=20+404,11=20= @@=20$node->connect_fails(=0A=20$node->connect_fails(=0A=20=09= "user=3Dscram_role=20require_auth=3D!scram-sha-256",=0A=20=09"SCRAM=20= authentication=20forbidden,=20fails=20with=20SCRAM=20auth",=0A-=09= expected_stderr=20=3D>=20qr/server=20requested=20SASL=20= authentication/);=0A+=09expected_stderr=20=3D>=20qr/server=20requested=20= SCRAM-SHA-256=20authentication/);=0A=20$node->connect_fails(=0A=20=09= "user=3Dscram_role=20require_auth=3D!password,!md5,!scram-sha-256",=0A=20= =09"multiple=20authentication=20types=20forbidden,=20fails=20with=20= SCRAM=20auth",=0A-=09expected_stderr=20=3D>=20qr/server=20requested=20= SASL=20authentication/);=0A+=09expected_stderr=20=3D>=20qr/server=20= requested=20SCRAM-SHA-256=20authentication/);=0A=20=0A=20#=20Test=20that=20= bad=20passwords=20are=20rejected.=0A=20$ENV{"PGPASSWORD"}=20=3D=20= 'badpass';=0A@@=20-465,13=20+465,13=20@@=20$node->connect_fails(=0A=20=09= "user=3Dscram_role=20require_auth=3D!scram-sha-256",=0A=20=09"password=20= authentication=20forbidden,=20fails=20with=20SCRAM=20auth",=0A=20=09= expected_stderr=20=3D>=0A-=09=20=20qr/authentication=20method=20= requirement=20"!scram-sha-256"=20failed:=20server=20requested=20SASL=20= authentication/=0A+=09=20=20qr/authentication=20method=20requirement=20= "!scram-sha-256"=20failed:=20server=20requested=20SCRAM-SHA-256=20= authentication/=0A=20);=0A=20$node->connect_fails(=0A=20=09= "user=3Dscram_role=20require_auth=3D!password,!md5,!scram-sha-256",=0A=20= =09"multiple=20authentication=20types=20forbidden,=20fails=20with=20= SCRAM=20auth",=0A=20=09expected_stderr=20=3D>=0A-=09=20=20= qr/authentication=20method=20requirement=20= "!password,!md5,!scram-sha-256"=20failed:=20server=20requested=20SASL=20= authentication/=0A+=09=20=20qr/authentication=20method=20requirement=20= "!password,!md5,!scram-sha-256"=20failed:=20server=20requested=20= SCRAM-SHA-256=20authentication/=0A=20);=0A=20=0A=20#=20Test=20= SYSTEM_USER=20<>=20NULL=20with=20parallel=20workers.=0Adiff=20--git=20= a/src/test/modules/Makefile=20b/src/test/modules/Makefile=0Aindex=20= 89e78b7d114..4e4be3fa511=20100644=0A---=20a/src/test/modules/Makefile=0A= +++=20b/src/test/modules/Makefile=0A@@=20-11,6=20+11,7=20@@=20SUBDIRS=20= =3D=20\=0A=20=09=09=20=20dummy_index_am=20\=0A=20=09=09=20=20= dummy_seclabel=20\=0A=20=09=09=20=20libpq_pipeline=20\=0A+=09=09=20=20= oauth_validator=20\=0A=20=09=09=20=20plsample=20\=0A=20=09=09=20=20= spgist_name_ops=20\=0A=20=09=09=20=20test_bloomfilter=20\=0Adiff=20--git=20= a/src/test/modules/meson.build=20b/src/test/modules/meson.build=0Aindex=20= a57077b682e..2b057451473=20100644=0A---=20a/src/test/modules/meson.build=0A= +++=20b/src/test/modules/meson.build=0A@@=20-9,6=20+9,7=20@@=20= subdir('gin')=0A=20subdir('injection_points')=0A=20= subdir('ldap_password_func')=0A=20subdir('libpq_pipeline')=0A= +subdir('oauth_validator')=0A=20subdir('plsample')=0A=20= subdir('spgist_name_ops')=0A=20subdir('ssl_passphrase_callback')=0Adiff=20= --git=20a/src/test/modules/oauth_validator/.gitignore=20= b/src/test/modules/oauth_validator/.gitignore=0Anew=20file=20mode=20= 100644=0Aindex=2000000000000..5dcb3ff9723=0A---=20/dev/null=0A+++=20= b/src/test/modules/oauth_validator/.gitignore=0A@@=20-0,0=20+1,4=20@@=0A= +#=20Generated=20subdirectories=0A+/log/=0A+/results/=0A+/tmp_check/=0A= diff=20--git=20a/src/test/modules/oauth_validator/Makefile=20= b/src/test/modules/oauth_validator/Makefile=0Anew=20file=20mode=20100644=0A= index=2000000000000..f297ed5c968=0A---=20/dev/null=0A+++=20= b/src/test/modules/oauth_validator/Makefile=0A@@=20-0,0=20+1,40=20@@=0A= +#------------------------------------------------------------------------= -=0A+#=0A+#=20Makefile=20for=20src/test/modules/oauth_validator=0A+#=0A= +#=20Portions=20Copyright=20(c)=201996-2024,=20PostgreSQL=20Global=20= Development=20Group=0A+#=20Portions=20Copyright=20(c)=201994,=20Regents=20= of=20the=20University=20of=20California=0A+#=0A+#=20= src/test/modules/oauth_validator/Makefile=0A+#=0A= +#------------------------------------------------------------------------= -=0A+=0A+MODULES=20=3D=20validator=20fail_validator=0A+PGFILEDESC=20=3D=20= "validator=20-=20test=20OAuth=20validator=20module"=0A+=0A+PROGRAM=20=3D=20= oauth_hook_client=0A+PGAPPICON=20=3D=20win32=0A+OBJS=20=3D=20$(WIN32RES)=20= oauth_hook_client.o=0A+=0A+PG_CPPFLAGS=20=3D=20-I$(libpq_srcdir)=0A= +PG_LIBS_INTERNAL=20+=3D=20$(libpq_pgport)=0A+=0A+NO_INSTALLCHECK=20=3D=20= 1=0A+=0A+TAP_TESTS=20=3D=201=0A+=0A+ifdef=20USE_PGXS=0A+PG_CONFIG=20=3D=20= pg_config=0A+PGXS=20:=3D=20$(shell=20$(PG_CONFIG)=20--pgxs)=0A+include=20= $(PGXS)=0A+else=0A+subdir=20=3D=20src/test/modules/oauth_validator=0A= +top_builddir=20=3D=20../../../..=0A+include=20= $(top_builddir)/src/Makefile.global=0A+include=20= $(top_srcdir)/contrib/contrib-global.mk=0A+=0A+export=20PYTHON=0A+export=20= with_libcurl=0A+export=20with_python=0A+=0A+endif=0Adiff=20--git=20= a/src/test/modules/oauth_validator/README=20= b/src/test/modules/oauth_validator/README=0Anew=20file=20mode=20100644=0A= index=2000000000000..138a8104622=0A---=20/dev/null=0A+++=20= b/src/test/modules/oauth_validator/README=0A@@=20-0,0=20+1,13=20@@=0A= +Test=20programs=20and=20libraries=20for=20OAuth=0A= +-------------------------------------=0A+=0A+This=20folder=20contains=20= tests=20for=20the=20client-=20and=20server-side=20OAuth=0A= +implementations.=20Most=20tests=20are=20run=20end-to-end=20to=20test=20= both=20simultaneously.=20The=0A+tests=20in=20t/001_server=20use=20a=20= mock=20OAuth=20authorization=20server,=20implemented=20jointly=0A+by=20= t/OAuth/Server.pm=20and=20t/oauth_server.py,=20to=20run=20the=20libpq=20= Device=0A+Authorization=20flow.=20The=20tests=20in=20t/002_client=20= exercise=20custom=20OAuth=20flows=20and=0A+don't=20need=20an=20= authorization=20server.=0A+=0A+Tests=20in=20this=20folder=20generally=20= require=20'oauth'=20to=20be=20present=20in=20PG_TEST_EXTRA,=0A+since=20= localhost=20HTTP=20servers=20will=20be=20started.=20A=20Python=20= installation=20is=20required=0A+to=20run=20the=20mock=20authorization=20= server.=0Adiff=20--git=20= a/src/test/modules/oauth_validator/fail_validator.c=20= b/src/test/modules/oauth_validator/fail_validator.c=0Anew=20file=20mode=20= 100644=0Aindex=2000000000000..f77a3e115c6=0A---=20/dev/null=0A+++=20= b/src/test/modules/oauth_validator/fail_validator.c=0A@@=20-0,0=20+1,42=20= @@=0A= +/*-----------------------------------------------------------------------= --=0A+=20*=0A+=20*=20fail_validator.c=0A+=20*=09=20=20Test=20module=20= for=20serverside=20OAuth=20token=20validation=20callbacks,=20which=20= always=0A+=20*=09=20=20fails=0A+=20*=0A+=20*=20Portions=20Copyright=20= (c)=201996-2024,=20PostgreSQL=20Global=20Development=20Group=0A+=20*=20= Portions=20Copyright=20(c)=201994,=20Regents=20of=20the=20University=20= of=20California=0A+=20*=0A+=20*=20= src/test/modules/oauth_validator/fail_validator.c=0A+=20*=0A+=20= *-------------------------------------------------------------------------= =0A+=20*/=0A+=0A+#include=20"postgres.h"=0A+=0A+#include=20"fmgr.h"=0A= +#include=20"libpq/oauth.h"=0A+=0A+PG_MODULE_MAGIC;=0A+=0A+static=20= ValidatorModuleResult=20*fail_token(ValidatorModuleState=20*state,=0A+=09= =09=09=09=09=09=09=09=09=09=20const=20char=20*token,=0A+=09=09=09=09=09=09= =09=09=09=09=20const=20char=20*role);=0A+=0A+/*=20Callback=20= implementations=20(we=20only=20need=20the=20main=20one)=20*/=0A+static=20= const=20OAuthValidatorCallbacks=20validator_callbacks=20=3D=20{=0A+=09= .validate_cb=20=3D=20fail_token,=0A+};=0A+=0A+const=20= OAuthValidatorCallbacks=20*=0A+_PG_oauth_validator_module_init(void)=0A= +{=0A+=09return=20&validator_callbacks;=0A+}=0A+=0A+static=20= ValidatorModuleResult=20*=0A+fail_token(ValidatorModuleState=20*state,=20= const=20char=20*token,=20const=20char=20*role)=0A+{=0A+=09elog(FATAL,=20= "fail_validator:=20sentinel=20error");=0A+=09pg_unreachable();=0A+}=0A= diff=20--git=20a/src/test/modules/oauth_validator/meson.build=20= b/src/test/modules/oauth_validator/meson.build=0Anew=20file=20mode=20= 100644=0Aindex=2000000000000..4b78c90557c=0A---=20/dev/null=0A+++=20= b/src/test/modules/oauth_validator/meson.build=0A@@=20-0,0=20+1,69=20@@=0A= +#=20Copyright=20(c)=202024,=20PostgreSQL=20Global=20Development=20Group=0A= +=0A+validator_sources=20=3D=20files(=0A+=20=20'validator.c',=0A+)=0A+=0A= +if=20host_system=20=3D=3D=20'windows'=0A+=20=20validator_sources=20+=3D=20= rc_lib_gen.process(win32ver_rc,=20extra_args:=20[=0A+=20=20=20=20= '--NAME',=20'validator',=0A+=20=20=20=20'--FILEDESC',=20'validator=20-=20= test=20OAuth=20validator=20module',])=0A+endif=0A+=0A+validator=20=3D=20= shared_module('validator',=0A+=20=20validator_sources,=0A+=20=20kwargs:=20= pg_test_mod_args,=0A+)=0A+test_install_libs=20+=3D=20validator=0A+=0A= +fail_validator_sources=20=3D=20files(=0A+=20=20'fail_validator.c',=0A+)=0A= +=0A+if=20host_system=20=3D=3D=20'windows'=0A+=20=20= fail_validator_sources=20+=3D=20rc_lib_gen.process(win32ver_rc,=20= extra_args:=20[=0A+=20=20=20=20'--NAME',=20'fail_validator',=0A+=20=20=20= =20'--FILEDESC',=20'fail_validator=20-=20failing=20OAuth=20validator=20= module',])=0A+endif=0A+=0A+fail_validator=20=3D=20= shared_module('fail_validator',=0A+=20=20fail_validator_sources,=0A+=20=20= kwargs:=20pg_test_mod_args,=0A+)=0A+test_install_libs=20+=3D=20= fail_validator=0A+=0A+oauth_hook_client_sources=20=3D=20files(=0A+=20=20= 'oauth_hook_client.c',=0A+)=0A+=0A+if=20host_system=20=3D=3D=20'windows'=0A= +=20=20oauth_hook_client_sources=20+=3D=20= rc_bin_gen.process(win32ver_rc,=20extra_args:=20[=0A+=20=20=20=20= '--NAME',=20'oauth_hook_client',=0A+=20=20=20=20'--FILEDESC',=20= 'oauth_hook_client=20-=20test=20program=20for=20libpq=20OAuth=20= hooks',])=0A+endif=0A+=0A+oauth_hook_client=20=3D=20= executable('oauth_hook_client',=0A+=20=20oauth_hook_client_sources,=0A+=20= =20dependencies:=20[frontend_code,=20libpq],=0A+=20=20kwargs:=20= default_bin_args=20+=20{=0A+=20=20=20=20'install':=20false,=0A+=20=20},=0A= +)=0A+testprep_targets=20+=3D=20oauth_hook_client=0A+=0A+tests=20+=3D=20= {=0A+=20=20'name':=20'oauth_validator',=0A+=20=20'sd':=20= meson.current_source_dir(),=0A+=20=20'bd':=20meson.current_build_dir(),=0A= +=20=20'tap':=20{=0A+=20=20=20=20'tests':=20[=0A+=20=20=20=20=20=20= 't/001_server.pl',=0A+=20=20=20=20=20=20't/002_client.pl',=0A+=20=20=20=20= ],=0A+=20=20=20=20'env':=20{=0A+=20=20=20=20=20=20'PYTHON':=20= python.path(),=0A+=20=20=20=20=20=20'with_libcurl':=20libcurl.found()=20= ?=20'yes'=20:=20'no',=0A+=20=20=20=20=20=20'with_python':=20'yes',=0A+=20= =20=20=20},=0A+=20=20},=0A+}=0Adiff=20--git=20= a/src/test/modules/oauth_validator/oauth_hook_client.c=20= b/src/test/modules/oauth_validator/oauth_hook_client.c=0Anew=20file=20= mode=20100644=0Aindex=2000000000000..fc003030ff8=0A---=20/dev/null=0A+++=20= b/src/test/modules/oauth_validator/oauth_hook_client.c=0A@@=20-0,0=20= +1,293=20@@=0A= +/*-----------------------------------------------------------------------= --=0A+=20*=0A+=20*=20oauth_hook_client.c=0A+=20*=09=09Test=20driver=20= for=20t/002_client.pl,=20which=20verifies=20OAuth=20hook=0A+=20*=09=09= functionality=20in=20libpq.=0A+=20*=0A+=20*=20Portions=20Copyright=20(c)=20= 1996-2024,=20PostgreSQL=20Global=20Development=20Group=0A+=20*=20= Portions=20Copyright=20(c)=201994,=20Regents=20of=20the=20University=20= of=20California=0A+=20*=0A+=20*=0A+=20*=20IDENTIFICATION=0A+=20*=09=09= src/test/modules/oauth_validator/oauth_hook_client.c=0A+=20*=0A+=20= *-------------------------------------------------------------------------= =0A+=20*/=0A+=0A+#include=20"postgres_fe.h"=0A+=0A+#include=20= =0A+=0A+#include=20"getopt_long.h"=0A+#include=20= "libpq-fe.h"=0A+=0A+static=20int=09handle_auth_data(PGauthData=20type,=20= PGconn=20*conn,=20void=20*data);=0A+static=20PostgresPollingStatusType=20= async_cb(PGconn=20*conn,=0A+=09=09=09=09=09=09=09=09=09=09=20=20= PGoauthBearerRequest=20*req,=0A+=09=09=09=09=09=09=09=09=09=09=20=20= pgsocket=20*altsock);=0A+static=20PostgresPollingStatusType=20= misbehave_cb(PGconn=20*conn,=0A+=09=09=09=09=09=09=09=09=09=09=09=20=20= PGoauthBearerRequest=20*req,=0A+=09=09=09=09=09=09=09=09=09=09=09=20=20= pgsocket=20*altsock);=0A+=0A+static=20void=0A+usage(char=20*argv[])=0A+{=0A= +=09printf("usage:=20%s=20[flags]=20CONNINFO\n\n",=20argv[0]);=0A+=0A+=09= printf("recognized=20flags:\n");=0A+=09printf("=20-h,=20--help=09=09=09=09= show=20this=20message\n");=0A+=09printf("=20--expected-scope=20SCOPE=09= fail=20if=20received=20scopes=20do=20not=20match=20SCOPE\n");=0A+=09= printf("=20--expected-uri=20URI=09=09fail=20if=20received=20= configuration=20link=20does=20not=20match=20URI\n");=0A+=09printf("=20= --misbehave=3DMODE=09=09have=20the=20hook=20fail=20required=20= postconditions\n"=0A+=09=09=20=20=20"=09=09=09=09=09=09(MODEs:=20= no-hook,=20fail-async,=20no-token,=20no-socket)\n");=0A+=09printf("=20= --no-hook=09=09=09=09don't=20install=20OAuth=20hooks\n");=0A+=09printf("=20= --hang-forever=09=09=09don't=20ever=20return=20a=20token=20(combine=20= with=20connect_timeout)\n");=0A+=09printf("=20--token=20TOKEN=09=09=09= use=20the=20provided=20TOKEN=20value\n");=0A+=09printf("=20= --stress-async=09=09=09busy-loop=20on=20PQconnectPoll=20rather=20than=20= polling\n");=0A+}=0A+=0A+/*=20--options=20*/=0A+static=20bool=20no_hook=20= =3D=20false;=0A+static=20bool=20hang_forever=20=3D=20false;=0A+static=20= bool=20stress_async=20=3D=20false;=0A+static=20const=20char=20= *expected_uri=20=3D=20NULL;=0A+static=20const=20char=20*expected_scope=20= =3D=20NULL;=0A+static=20const=20char=20*misbehave_mode=20=3D=20NULL;=0A= +static=20char=20*token=20=3D=20NULL;=0A+=0A+int=0A+main(int=20argc,=20= char=20*argv[])=0A+{=0A+=09static=20const=20struct=20option=20= long_options[]=20=3D=20{=0A+=09=09{"help",=20no_argument,=20NULL,=20= 'h'},=0A+=0A+=09=09{"expected-scope",=20required_argument,=20NULL,=20= 1000},=0A+=09=09{"expected-uri",=20required_argument,=20NULL,=201001},=0A= +=09=09{"no-hook",=20no_argument,=20NULL,=201002},=0A+=09=09{"token",=20= required_argument,=20NULL,=201003},=0A+=09=09{"hang-forever",=20= no_argument,=20NULL,=201004},=0A+=09=09{"misbehave",=20= required_argument,=20NULL,=201005},=0A+=09=09{"stress-async",=20= no_argument,=20NULL,=201006},=0A+=09=09{0}=0A+=09};=0A+=0A+=09const=20= char=20*conninfo;=0A+=09PGconn=09=20=20=20*conn;=0A+=09int=09=09=09c;=0A= +=0A+=09while=20((c=20=3D=20getopt_long(argc,=20argv,=20"h",=20= long_options,=20NULL))=20!=3D=20-1)=0A+=09{=0A+=09=09switch=20(c)=0A+=09=09= {=0A+=09=09=09case=20'h':=0A+=09=09=09=09usage(argv);=0A+=09=09=09=09= return=200;=0A+=0A+=09=09=09case=201000:=09=09=09/*=20--expected-scope=20= */=0A+=09=09=09=09expected_scope=20=3D=20optarg;=0A+=09=09=09=09break;=0A= +=0A+=09=09=09case=201001:=09=09=09/*=20--expected-uri=20*/=0A+=09=09=09=09= expected_uri=20=3D=20optarg;=0A+=09=09=09=09break;=0A+=0A+=09=09=09case=20= 1002:=09=09=09/*=20--no-hook=20*/=0A+=09=09=09=09no_hook=20=3D=20true;=0A= +=09=09=09=09break;=0A+=0A+=09=09=09case=201003:=09=09=09/*=20--token=20= */=0A+=09=09=09=09token=20=3D=20optarg;=0A+=09=09=09=09break;=0A+=0A+=09=09= =09case=201004:=09=09=09/*=20--hang-forever=20*/=0A+=09=09=09=09= hang_forever=20=3D=20true;=0A+=09=09=09=09break;=0A+=0A+=09=09=09case=20= 1005:=09=09=09/*=20--misbehave=20*/=0A+=09=09=09=09misbehave_mode=20=3D=20= optarg;=0A+=09=09=09=09break;=0A+=0A+=09=09=09case=201006:=09=09=09/*=20= --stress-async=20*/=0A+=09=09=09=09stress_async=20=3D=20true;=0A+=09=09=09= =09break;=0A+=0A+=09=09=09default:=0A+=09=09=09=09usage(argv);=0A+=09=09=09= =09return=201;=0A+=09=09}=0A+=09}=0A+=0A+=09if=20(argc=20!=3D=20optind=20= +=201)=0A+=09{=0A+=09=09usage(argv);=0A+=09=09return=201;=0A+=09}=0A+=0A= +=09conninfo=20=3D=20argv[optind];=0A+=0A+=09/*=20Set=20up=20our=20OAuth=20= hooks.=20*/=0A+=09PQsetAuthDataHook(handle_auth_data);=0A+=0A+=09/*=20= Connect.=20(All=20the=20actual=20work=20is=20in=20the=20hook.)=20*/=0A+=09= if=20(stress_async)=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20Perform=20an=20= asynchronous=20connection,=20busy-looping=20on=20PQconnectPoll()=0A+=09=09= =20*=20without=20actually=20waiting=20on=20socket=20events.=20This=20= stresses=20code=20paths=0A+=09=09=20*=20that=20rely=20on=20asynchronous=20= work=20to=20be=20done=20before=20continuing=20with=0A+=09=09=20*=20the=20= next=20step=20in=20the=20flow.=0A+=09=09=20*/=0A+=09=09= PostgresPollingStatusType=20res;=0A+=0A+=09=09conn=20=3D=20= PQconnectStart(conninfo);=0A+=0A+=09=09do=0A+=09=09{=0A+=09=09=09res=20=3D= =20PQconnectPoll(conn);=0A+=09=09}=20while=20(res=20!=3D=20= PGRES_POLLING_FAILED=20&&=20res=20!=3D=20PGRES_POLLING_OK);=0A+=09}=0A+=09= else=0A+=09{=0A+=09=09/*=20Perform=20a=20standard=20synchronous=20= connection.=20*/=0A+=09=09conn=20=3D=20PQconnectdb(conninfo);=0A+=09}=0A= +=0A+=09if=20(PQstatus(conn)=20!=3D=20CONNECTION_OK)=0A+=09{=0A+=09=09= fprintf(stderr,=20"connection=20to=20database=20failed:=20%s\n",=0A+=09=09= =09=09PQerrorMessage(conn));=0A+=09=09PQfinish(conn);=0A+=09=09return=20= 1;=0A+=09}=0A+=0A+=09printf("connection=20succeeded\n");=0A+=09= PQfinish(conn);=0A+=09return=200;=0A+}=0A+=0A+/*=0A+=20*=20= PQauthDataHook=20implementation.=20Replaces=20the=20default=20client=20= flow=20by=20handling=0A+=20*=20PQAUTHDATA_OAUTH_BEARER_TOKEN.=0A+=20*/=0A= +static=20int=0A+handle_auth_data(PGauthData=20type,=20PGconn=20*conn,=20= void=20*data)=0A+{=0A+=09PGoauthBearerRequest=20*req=20=3D=20data;=0A+=0A= +=09if=20(no_hook=20||=20(type=20!=3D=20PQAUTHDATA_OAUTH_BEARER_TOKEN))=0A= +=09=09return=200;=0A+=0A+=09if=20(hang_forever)=0A+=09{=0A+=09=09/*=20= Start=20asynchronous=20processing.=20*/=0A+=09=09req->async=20=3D=20= async_cb;=0A+=09=09return=201;=0A+=09}=0A+=0A+=09if=20(misbehave_mode)=0A= +=09{=0A+=09=09if=20(strcmp(misbehave_mode,=20"no-hook")=20!=3D=200)=0A+=09= =09=09req->async=20=3D=20misbehave_cb;=0A+=09=09return=201;=0A+=09}=0A+=0A= +=09if=20(expected_uri)=0A+=09{=0A+=09=09if=20= (!req->openid_configuration)=0A+=09=09{=0A+=09=09=09fprintf(stderr,=20= "expected=20URI=20\"%s\",=20got=20NULL\n",=20expected_uri);=0A+=09=09=09= return=20-1;=0A+=09=09}=0A+=0A+=09=09if=20(strcmp(expected_uri,=20= req->openid_configuration)=20!=3D=200)=0A+=09=09{=0A+=09=09=09= fprintf(stderr,=20"expected=20URI=20\"%s\",=20got=20\"%s\"\n",=20= expected_uri,=20req->openid_configuration);=0A+=09=09=09return=20-1;=0A+=09= =09}=0A+=09}=0A+=0A+=09if=20(expected_scope)=0A+=09{=0A+=09=09if=20= (!req->scope)=0A+=09=09{=0A+=09=09=09fprintf(stderr,=20"expected=20scope=20= \"%s\",=20got=20NULL\n",=20expected_scope);=0A+=09=09=09return=20-1;=0A+=09= =09}=0A+=0A+=09=09if=20(strcmp(expected_scope,=20req->scope)=20!=3D=200)=0A= +=09=09{=0A+=09=09=09fprintf(stderr,=20"expected=20scope=20\"%s\",=20got=20= \"%s\"\n",=20expected_scope,=20req->scope);=0A+=09=09=09return=20-1;=0A+=09= =09}=0A+=09}=0A+=0A+=09req->token=20=3D=20token;=0A+=09return=201;=0A+}=0A= +=0A+static=20PostgresPollingStatusType=0A+async_cb(PGconn=20*conn,=20= PGoauthBearerRequest=20*req,=20pgsocket=20*altsock)=0A+{=0A+=09if=20= (hang_forever)=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20This=20code=20tests=20= that=20nothing=20is=20interfering=20with=20libpq's=20handling=0A+=09=09=20= *=20of=20connect_timeout.=0A+=09=09=20*/=0A+=09=09static=20pgsocket=20= sock=20=3D=20PGINVALID_SOCKET;=0A+=0A+=09=09if=20(sock=20=3D=3D=20= PGINVALID_SOCKET)=0A+=09=09{=0A+=09=09=09/*=20First=20call.=20Create=20= an=20unbound=20socket=20to=20wait=20on.=20*/=0A+#ifdef=20WIN32=0A+=09=09=09= WSADATA=09=09wsaData;=0A+=09=09=09int=09=09=09err;=0A+=0A+=09=09=09err=20= =3D=20WSAStartup(MAKEWORD(2,=202),=20&wsaData);=0A+=09=09=09if=20(err)=0A= +=09=09=09{=0A+=09=09=09=09perror("WSAStartup=20failed");=0A+=09=09=09=09= return=20PGRES_POLLING_FAILED;=0A+=09=09=09}=0A+#endif=0A+=09=09=09sock=20= =3D=20socket(AF_INET,=20SOCK_DGRAM,=200);=0A+=09=09=09if=20(sock=20=3D=3D=20= PGINVALID_SOCKET)=0A+=09=09=09{=0A+=09=09=09=09perror("failed=20to=20= create=20datagram=20socket");=0A+=09=09=09=09return=20= PGRES_POLLING_FAILED;=0A+=09=09=09}=0A+=09=09}=0A+=0A+=09=09/*=20Make=20= libpq=20wait=20on=20the=20(unreadable)=20socket.=20*/=0A+=09=09*altsock=20= =3D=20sock;=0A+=09=09return=20PGRES_POLLING_READING;=0A+=09}=0A+=0A+=09= req->token=20=3D=20token;=0A+=09return=20PGRES_POLLING_OK;=0A+}=0A+=0A= +static=20PostgresPollingStatusType=0A+misbehave_cb(PGconn=20*conn,=20= PGoauthBearerRequest=20*req,=20pgsocket=20*altsock)=0A+{=0A+=09if=20= (strcmp(misbehave_mode,=20"fail-async")=20=3D=3D=200)=0A+=09{=0A+=09=09= /*=20Just=20fail=20"normally".=20*/=0A+=09=09return=20= PGRES_POLLING_FAILED;=0A+=09}=0A+=09else=20if=20(strcmp(misbehave_mode,=20= "no-token")=20=3D=3D=200)=0A+=09{=0A+=09=09/*=20Callbacks=20must=20= assign=20req->token=20before=20returning=20OK.=20*/=0A+=09=09return=20= PGRES_POLLING_OK;=0A+=09}=0A+=09else=20if=20(strcmp(misbehave_mode,=20= "no-socket")=20=3D=3D=200)=0A+=09{=0A+=09=09/*=20Callbacks=20must=20= assign=20*altsock=20before=20asking=20for=20polling.=20*/=0A+=09=09= return=20PGRES_POLLING_READING;=0A+=09}=0A+=09else=0A+=09{=0A+=09=09= fprintf(stderr,=20"unrecognized=20--misbehave=20mode:=20%s\n",=20= misbehave_mode);=0A+=09=09exit(1);=0A+=09}=0A+}=0Adiff=20--git=20= a/src/test/modules/oauth_validator/t/001_server.pl=20= b/src/test/modules/oauth_validator/t/001_server.pl=0Anew=20file=20mode=20= 100644=0Aindex=2000000000000..f0b918390fd=0A---=20/dev/null=0A+++=20= b/src/test/modules/oauth_validator/t/001_server.pl=0A@@=20-0,0=20+1,566=20= @@=0A+=0A+#=0A+#=20Tests=20the=20libpq=20builtin=20OAuth=20flow,=20as=20= well=20as=20server-side=20HBA=20and=20validator=0A+#=20setup.=0A+#=0A+#=20= Copyright=20(c)=202021-2024,=20PostgreSQL=20Global=20Development=20Group=0A= +#=0A+=0A+use=20strict;=0A+use=20warnings=20FATAL=20=3D>=20'all';=0A+=0A= +use=20JSON::PP=20=20=20=20=20qw(encode_json);=0A+use=20MIME::Base64=20= qw(encode_base64);=0A+use=20PostgreSQL::Test::Cluster;=0A+use=20= PostgreSQL::Test::Utils;=0A+use=20Test::More;=0A+=0A+use=20FindBin;=0A= +use=20lib=20$FindBin::RealBin;=0A+=0A+use=20OAuth::Server;=0A+=0A+if=20= (!$ENV{PG_TEST_EXTRA}=20||=20$ENV{PG_TEST_EXTRA}=20!~=20/\boauth\b/)=0A= +{=0A+=09plan=20skip_all=20=3D>=0A+=09=20=20'Potentially=20unsafe=20test=20= oauth=20not=20enabled=20in=20PG_TEST_EXTRA';=0A+}=0A+=0A+if=20= ($ENV{with_libcurl}=20ne=20'yes')=0A+{=0A+=09plan=20skip_all=20=3D>=20= 'client-side=20OAuth=20not=20supported=20by=20this=20build';=0A+}=0A+=0A= +if=20($ENV{with_python}=20ne=20'yes')=0A+{=0A+=09plan=20skip_all=20=3D>=20= 'OAuth=20tests=20require=20--with-python=20to=20run';=0A+}=0A+=0A+my=20= $node=20=3D=20PostgreSQL::Test::Cluster->new('primary');=0A+$node->init;=0A= +$node->append_conf('postgresql.conf',=20"log_connections=20=3D=20= on\n");=0A+$node->append_conf('postgresql.conf',=0A+=09= "oauth_validator_libraries=20=3D=20'validator'\n");=0A+$node->start;=0A+=0A= +$node->safe_psql('postgres',=20'CREATE=20USER=20test;');=0A= +$node->safe_psql('postgres',=20'CREATE=20USER=20testalt;');=0A= +$node->safe_psql('postgres',=20'CREATE=20USER=20testparam;');=0A+=0A+#=20= Save=20a=20background=20connection=20for=20later=20configuration=20= changes.=0A+my=20$bgconn=20=3D=20$node->background_psql('postgres');=0A+=0A= +my=20$webserver=20=3D=20OAuth::Server->new();=0A+$webserver->run();=0A+=0A= +END=0A+{=0A+=09my=20$exit_code=20=3D=20$?;=0A+=0A+=09$webserver->stop()=20= if=20defined=20$webserver;=20=20=20=20#=20might=20have=20been=20SKIP'd=0A= +=0A+=09$?=20=3D=20$exit_code;=0A+}=0A+=0A+my=20$port=20=3D=20= $webserver->port();=0A+my=20$issuer=20=3D=20"http://localhost:$port";=0A= +=0A+unlink($node->data_dir=20.=20'/pg_hba.conf');=0A= +$node->append_conf(=0A+=09'pg_hba.conf',=20qq{=0A+local=20all=20test=20=20= =20=20=20=20oauth=20issuer=3D"$issuer"=20=20=20=20=20=20=20scope=3D"openid= =20postgres"=0A+local=20all=20testalt=20=20=20oauth=20= issuer=3D"$issuer/.well-known/oauth-authorization-server/alternate"=20= scope=3D"openid=20postgres=20alt"=0A+local=20all=20testparam=20oauth=20= issuer=3D"$issuer/param"=20scope=3D"openid=20postgres"=0A+});=0A= +$node->reload;=0A+=0A+my=20$log_start=20=3D=20= $node->wait_for_log(qr/reloading=20configuration=20files/);=0A+=0A+#=20= Check=20pg_hba_file_rules()=20support.=0A+my=20$contents=20=3D=20= $bgconn->query_safe(=0A+=09qq(SELECT=20rule_number,=20auth_method,=20= options=0A+=09=09=20FROM=20pg_hba_file_rules=0A+=09=09=20ORDER=20BY=20= rule_number;));=0A+is(=20$contents,=0A+=09= qq{1|oauth|\{issuer=3D$issuer,"scope=3Dopenid=20= postgres",validator=3Dvalidator\}=0A= +2|oauth|\{issuer=3D$issuer/.well-known/oauth-authorization-server/alterna= te,"scope=3Dopenid=20postgres=20alt",validator=3Dvalidator\}=0A= +3|oauth|\{issuer=3D$issuer/param,"scope=3Dopenid=20= postgres",validator=3Dvalidator\}},=0A+=09"pg_hba_file_rules=20recreates=20= OAuth=20HBA=20settings");=0A+=0A+#=20To=20test=20against=20HTTP=20rather=20= than=20HTTPS,=20we=20need=20to=20enable=20PGOAUTHDEBUG.=20But=0A+#=20= first,=20check=20to=20make=20sure=20the=20client=20refuses=20such=20= connections=20by=20default.=0A+$node->connect_fails(=0A+=09"user=3Dtest=20= dbname=3Dpostgres=20oauth_issuer=3D$issuer=20= oauth_client_id=3Df02c6361-0635",=0A+=09"HTTPS=20is=20required=20without=20= debug=20mode",=0A+=09expected_stderr=20=3D>=0A+=09=20=20qr@OAuth=20= discovery=20URI=20"\Q$issuer\E/.well-known/openid-configuration"=20must=20= use=20HTTPS@=0A+);=0A+=0A+$ENV{PGOAUTHDEBUG}=20=3D=20"UNSAFE";=0A+=0A+my=20= $user=20=3D=20"test";=0A+$node->connect_ok(=0A+=09"user=3D$user=20= dbname=3Dpostgres=20oauth_issuer=3D$issuer=20= oauth_client_id=3Df02c6361-0635",=0A+=09"connect=20as=20test",=0A+=09= expected_stderr=20=3D>=0A+=09=20=20qr@Visit=20https://example\.com/=20= and=20enter=20the=20code:=20postgresuser@,=0A+=09log_like=20=3D>=20[=0A+=09= =09qr/oauth_validator:=20token=3D"9243959234",=20role=3D"$user"/,=0A+=09=09= qr/oauth_validator:=20issuer=3D"\Q$issuer\E",=20scope=3D"openid=20= postgres"/,=0A+=09=09qr/connection=20authenticated:=20identity=3D"test"=20= method=3Doauth/,=0A+=09=09qr/connection=20authorized/,=0A+=09]);=0A+=0A= +#=20The=20/alternate=20issuer=20uses=20slightly=20different=20= parameters,=20along=20with=20an=0A+#=20OAuth-style=20discovery=20= document.=0A+$user=20=3D=20"testalt";=0A+$node->connect_ok(=0A+=09= "user=3D$user=20dbname=3Dpostgres=20oauth_issuer=3D$issuer/alternate=20= oauth_client_id=3Df02c6361-0636",=0A+=09"connect=20as=20testalt",=0A+=09= expected_stderr=20=3D>=0A+=09=20=20qr@Visit=20https://example\.org/=20= and=20enter=20the=20code:=20postgresuser@,=0A+=09log_like=20=3D>=20[=0A+=09= =09qr/oauth_validator:=20token=3D"9243959234-alt",=20role=3D"$user"/,=0A= +=09=09qr|oauth_validator:=20= issuer=3D"\Q$issuer/.well-known/oauth-authorization-server/alternate\E",=20= scope=3D"openid=20postgres=20alt"|,=0A+=09=09qr/connection=20= authenticated:=20identity=3D"testalt"=20method=3Doauth/,=0A+=09=09= qr/connection=20authorized/,=0A+=09]);=0A+=0A+#=20The=20issuer=20linked=20= by=20the=20server=20must=20match=20the=20client's=20oauth_issuer=20= setting.=0A+$node->connect_fails(=0A+=09"user=3D$user=20dbname=3Dpostgres=20= oauth_issuer=3D$issuer=20oauth_client_id=3Df02c6361-0636",=0A+=09= "oauth_issuer=20must=20match=20discovery",=0A+=09expected_stderr=20=3D>=0A= +=09=20=20qr@server's=20discovery=20document=20at=20= \Q$issuer/.well-known/oauth-authorization-server/alternate\E=20\(issuer=20= "\Q$issuer/alternate\E"\)=20is=20incompatible=20with=20oauth_issuer=20= \(\Q$issuer\E\)@=0A+);=0A+=0A+#=20Test=20require_auth=20settings=20= against=20OAUTHBEARER.=0A+my=20@cases=20=3D=20(=0A+=09{=20require_auth=20= =3D>=20"oauth"=20},=0A+=09{=20require_auth=20=3D>=20= "oauth,scram-sha-256"=20},=0A+=09{=20require_auth=20=3D>=20= "password,oauth"=20},=0A+=09{=20require_auth=20=3D>=20"none,oauth"=20},=0A= +=09{=20require_auth=20=3D>=20"!scram-sha-256"=20},=0A+=09{=20= require_auth=20=3D>=20"!none"=20},=0A+=0A+=09{=0A+=09=09require_auth=20= =3D>=20"!oauth",=0A+=09=09failure=20=3D>=20qr/server=20requested=20= OAUTHBEARER=20authentication/=0A+=09},=0A+=09{=0A+=09=09require_auth=20= =3D>=20"scram-sha-256",=0A+=09=09failure=20=3D>=20qr/server=20requested=20= OAUTHBEARER=20authentication/=0A+=09},=0A+=09{=0A+=09=09require_auth=20= =3D>=20"!password,!oauth",=0A+=09=09failure=20=3D>=20qr/server=20= requested=20OAUTHBEARER=20authentication/=0A+=09},=0A+=09{=0A+=09=09= require_auth=20=3D>=20"none",=0A+=09=09failure=20=3D>=20qr/server=20= requested=20SASL=20authentication/=0A+=09},=0A+=09{=0A+=09=09= require_auth=20=3D>=20"!oauth,!scram-sha-256",=0A+=09=09failure=20=3D>=20= qr/server=20requested=20SASL=20authentication/=0A+=09});=0A+=0A+$user=20= =3D=20"test";=0A+foreach=20my=20$c=20(@cases)=0A+{=0A+=09my=20$connstr=20= =3D=0A+=09=20=20"user=3D$user=20dbname=3Dpostgres=20oauth_issuer=3D$issuer= =20oauth_client_id=3Df02c6361-0635=20require_auth=3D$c->{'require_auth'}";= =0A+=0A+=09if=20(defined=20$c->{'failure'})=0A+=09{=0A+=09=09= $node->connect_fails(=0A+=09=09=09$connstr,=0A+=09=09=09= "require_auth=3D$c->{'require_auth'}=20fails",=0A+=09=09=09= expected_stderr=20=3D>=20$c->{'failure'});=0A+=09}=0A+=09else=0A+=09{=0A= +=09=09$node->connect_ok(=0A+=09=09=09$connstr,=0A+=09=09=09= "require_auth=3D$c->{'require_auth'}=20succeeds",=0A+=09=09=09= expected_stderr=20=3D>=0A+=09=09=09=20=20qr@Visit=20= https://example\.com/=20and=20enter=20the=20code:=20postgresuser@=0A+=09=09= );=0A+=09}=0A+}=0A+=0A+#=20Make=20sure=20the=20client_id=20and=20secret=20= are=20correctly=20encoded.=20$vschars=20contains=0A+#=20every=20allowed=20= character=20for=20a=20client_id/_secret=20(the=20"VSCHAR"=20class).=0A+#=20= $vschars_esc=20is=20additionally=20backslash-escaped=20for=20inclusion=20= in=20a=0A+#=20single-quoted=20connection=20string.=0A+my=20$vschars=20=3D=0A= +=20=20"=20= !\"#\$%&'()*+,-./0123456789:;<=3D>?\@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcd= efghijklmnopqrstuvwxyz{|}~";=0A+my=20$vschars_esc=20=3D=0A+=20=20"=20= !\"#\$%&\\'()*+,-./0123456789:;<=3D>?\@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`= abcdefghijklmnopqrstuvwxyz{|}~";=0A+=0A+$node->connect_ok(=0A+=09= "user=3D$user=20dbname=3Dpostgres=20oauth_issuer=3D$issuer=20= oauth_client_id=3D'$vschars_esc'",=0A+=09"escapable=20characters:=20= client_id",=0A+=09expected_stderr=20=3D>=0A+=09=20=20qr@Visit=20= https://example\.com/=20and=20enter=20the=20code:=20postgresuser@);=0A= +$node->connect_ok(=0A+=09"user=3D$user=20dbname=3Dpostgres=20= oauth_issuer=3D$issuer=20oauth_client_id=3D'$vschars_esc'=20= oauth_client_secret=3D'$vschars_esc'",=0A+=09"escapable=20characters:=20= client_id=20and=20secret",=0A+=09expected_stderr=20=3D>=0A+=09=20=20= qr@Visit=20https://example\.com/=20and=20enter=20the=20code:=20= postgresuser@);=0A+=0A+#=0A+#=20Further=20tests=20rely=20on=20support=20= for=20specific=20behaviors=20in=20oauth_server.py.=20To=0A+#=20trigger=20= these=20behaviors,=20we=20ask=20for=20the=20special=20issuer=20.../param=20= (which=20is=20set=0A+#=20up=20in=20HBA=20for=20the=20testparam=20user)=20= and=20encode=20magic=20instructions=20into=20the=0A+#=20oauth_client_id.=0A= +#=0A+=0A+my=20$common_connstr=20=3D=0A+=20=20"user=3Dtestparam=20= dbname=3Dpostgres=20oauth_issuer=3D$issuer/param=20";=0A+my=20= $base_connstr=20=3D=20$common_connstr;=0A+=0A+sub=20connstr=0A+{=0A+=09= my=20(%params)=20=3D=20@_;=0A+=0A+=09my=20$json=20=3D=20= encode_json(\%params);=0A+=09my=20$encoded=20=3D=20encode_base64($json,=20= "");=0A+=0A+=09return=20"$base_connstr=20oauth_client_id=3D$encoded";=0A= +}=0A+=0A+#=20Make=20sure=20the=20param=20system=20works=20end-to-end=20= first.=0A+$node->connect_ok(=0A+=09connstr(),=0A+=09"connect=20to=20= /param",=0A+=09expected_stderr=20=3D>=0A+=09=20=20qr@Visit=20= https://example\.com/=20and=20enter=20the=20code:=20postgresuser@);=0A+=0A= +$node->connect_ok(=0A+=09connstr(stage=20=3D>=20'token',=20retries=20=3D>= =201),=0A+=09"token=20retry",=0A+=09expected_stderr=20=3D>=0A+=09=20=20= qr@Visit=20https://example\.com/=20and=20enter=20the=20code:=20= postgresuser@);=0A+$node->connect_ok(=0A+=09connstr(stage=20=3D>=20= 'token',=20retries=20=3D>=202),=0A+=09"token=20retry=20(twice)",=0A+=09= expected_stderr=20=3D>=0A+=09=20=20qr@Visit=20https://example\.com/=20= and=20enter=20the=20code:=20postgresuser@);=0A+$node->connect_ok(=0A+=09= connstr(stage=20=3D>=20'all',=20retries=20=3D>=201,=20interval=20=3D>=20= 2),=0A+=09"token=20retry=20(two=20second=20interval)",=0A+=09= expected_stderr=20=3D>=0A+=09=20=20qr@Visit=20https://example\.com/=20= and=20enter=20the=20code:=20postgresuser@);=0A+$node->connect_ok(=0A+=09= connstr(stage=20=3D>=20'all',=20retries=20=3D>=201,=20interval=20=3D>=20= JSON::PP::null),=0A+=09"token=20retry=20(default=20interval)",=0A+=09= expected_stderr=20=3D>=0A+=09=20=20qr@Visit=20https://example\.com/=20= and=20enter=20the=20code:=20postgresuser@);=0A+=0A+$node->connect_ok(=0A= +=09connstr(stage=20=3D>=20'all',=20content_type=20=3D>=20= 'application/json;charset=3Dutf-8'),=0A+=09"content=20type=20with=20= charset",=0A+=09expected_stderr=20=3D>=0A+=09=20=20qr@Visit=20= https://example\.com/=20and=20enter=20the=20code:=20postgresuser@);=0A= +$node->connect_ok(=0A+=09connstr(=0A+=09=09stage=20=3D>=20'all',=0A+=09=09= content_type=20=3D>=20"application/json=20\t;\t=20charset=3Dutf-8"),=0A+=09= "content=20type=20with=20charset=20(whitespace)",=0A+=09expected_stderr=20= =3D>=0A+=09=20=20qr@Visit=20https://example\.com/=20and=20enter=20the=20= code:=20postgresuser@);=0A+$node->connect_ok(=0A+=09connstr(stage=20=3D>=20= 'device',=20uri_spelling=20=3D>=20"verification_url"),=0A+=09= "alternative=20spelling=20of=20verification_uri",=0A+=09expected_stderr=20= =3D>=0A+=09=20=20qr@Visit=20https://example\.com/=20and=20enter=20the=20= code:=20postgresuser@);=0A+=0A+$node->connect_fails(=0A+=09connstr(stage=20= =3D>=20'device',=20huge_response=20=3D>=20JSON::PP::true),=0A+=09"bad=20= device=20authz=20response:=20overlarge=20JSON",=0A+=09expected_stderr=20= =3D>=0A+=09=20=20qr/failed=20to=20obtain=20device=20authorization:=20= response=20is=20too=20large/);=0A+$node->connect_fails(=0A+=09= connstr(stage=20=3D>=20'token',=20huge_response=20=3D>=20= JSON::PP::true),=0A+=09"bad=20token=20response:=20overlarge=20JSON",=0A+=09= expected_stderr=20=3D>=0A+=09=20=20qr/failed=20to=20obtain=20access=20= token:=20response=20is=20too=20large/);=0A+=0A+$node->connect_fails(=0A+=09= connstr(stage=20=3D>=20'device',=20content_type=20=3D>=20'text/plain'),=0A= +=09"bad=20device=20authz=20response:=20wrong=20content=20type",=0A+=09= expected_stderr=20=3D>=0A+=09=20=20qr/failed=20to=20parse=20device=20= authorization:=20unexpected=20content=20type/);=0A+$node->connect_fails(=0A= +=09connstr(stage=20=3D>=20'token',=20content_type=20=3D>=20= 'text/plain'),=0A+=09"bad=20token=20response:=20wrong=20content=20type",=0A= +=09expected_stderr=20=3D>=0A+=09=20=20qr/failed=20to=20parse=20access=20= token=20response:=20unexpected=20content=20type/);=0A= +$node->connect_fails(=0A+=09connstr(stage=20=3D>=20'token',=20= content_type=20=3D>=20'application/jsonx'),=0A+=09"bad=20token=20= response:=20wrong=20content=20type=20(correct=20prefix)",=0A+=09= expected_stderr=20=3D>=0A+=09=20=20qr/failed=20to=20parse=20access=20= token=20response:=20unexpected=20content=20type/);=0A+=0A= +$node->connect_fails(=0A+=09connstr(=0A+=09=09stage=20=3D>=20'all',=0A+=09= =09interval=20=3D>=20~0,=0A+=09=09retries=20=3D>=201,=0A+=09=09= retry_code=20=3D>=20"slow_down"),=0A+=09"bad=20token=20response:=20= server=20overflows=20the=20device=20authz=20interval",=0A+=09= expected_stderr=20=3D>=0A+=09=20=20qr/failed=20to=20obtain=20access=20= token:=20slow_down=20interval=20overflow/);=0A+=0A+$node->connect_fails(=0A= +=09connstr(stage=20=3D>=20'token',=20error_code=20=3D>=20= "invalid_grant"),=0A+=09"bad=20token=20response:=20invalid_grant,=20no=20= description",=0A+=09expected_stderr=20=3D>=20qr/failed=20to=20obtain=20= access=20token:=20\(invalid_grant\)/);=0A+$node->connect_fails(=0A+=09= connstr(=0A+=09=09stage=20=3D>=20'token',=0A+=09=09error_code=20=3D>=20= "invalid_grant",=0A+=09=09error_desc=20=3D>=20"grant=20expired"),=0A+=09= "bad=20token=20response:=20expired=20grant",=0A+=09expected_stderr=20=3D>=0A= +=09=20=20qr/failed=20to=20obtain=20access=20token:=20grant=20expired=20= \(invalid_grant\)/);=0A+$node->connect_fails(=0A+=09connstr(=0A+=09=09= stage=20=3D>=20'token',=0A+=09=09error_code=20=3D>=20"invalid_client",=0A= +=09=09error_status=20=3D>=20401),=0A+=09"bad=20token=20response:=20= client=20authentication=20failure,=20default=20description",=0A+=09= expected_stderr=20=3D>=0A+=09=20=20qr/failed=20to=20obtain=20access=20= token:=20provider=20requires=20client=20authentication,=20and=20no=20= oauth_client_secret=20is=20set=20\(invalid_client\)/=0A+);=0A= +$node->connect_fails(=0A+=09connstr(=0A+=09=09stage=20=3D>=20'token',=0A= +=09=09error_code=20=3D>=20"invalid_client",=0A+=09=09error_status=20=3D>=20= 401,=0A+=09=09error_desc=20=3D>=20"authn=20failure"),=0A+=09"bad=20token=20= response:=20client=20authentication=20failure,=20provided=20= description",=0A+=09expected_stderr=20=3D>=0A+=09=20=20qr/failed=20to=20= obtain=20access=20token:=20authn=20failure=20\(invalid_client\)/);=0A+=0A= +$node->connect_fails(=0A+=09connstr(stage=20=3D>=20'token',=20token=20= =3D>=20""),=0A+=09"server=20rejects=20access:=20empty=20token",=0A+=09= expected_stderr=20=3D>=20qr/bearer=20authentication=20failed/);=0A= +$node->connect_fails(=0A+=09connstr(stage=20=3D>=20'token',=20token=20= =3D>=20"****"),=0A+=09"server=20rejects=20access:=20invalid=20token=20= contents",=0A+=09expected_stderr=20=3D>=20qr/bearer=20authentication=20= failed/);=0A+=0A+#=20Test=20behavior=20of=20the=20oauth_client_secret.=0A= +$base_connstr=20=3D=20"$common_connstr=20oauth_client_secret=3D''";=0A+=0A= +$node->connect_ok(=0A+=09connstr(stage=20=3D>=20'all',=20= expected_secret=20=3D>=20''),=0A+=09"empty=20oauth_client_secret",=0A+=09= expected_stderr=20=3D>=0A+=09=20=20qr@Visit=20https://example\.com/=20= and=20enter=20the=20code:=20postgresuser@);=0A+=0A+$base_connstr=20=3D=20= "$common_connstr=20oauth_client_secret=3D'$vschars_esc'";=0A+=0A= +$node->connect_ok(=0A+=09connstr(stage=20=3D>=20'all',=20= expected_secret=20=3D>=20$vschars),=0A+=09"nonempty=20= oauth_client_secret",=0A+=09expected_stderr=20=3D>=0A+=09=20=20qr@Visit=20= https://example\.com/=20and=20enter=20the=20code:=20postgresuser@);=0A+=0A= +$node->connect_fails(=0A+=09connstr(=0A+=09=09stage=20=3D>=20'token',=0A= +=09=09error_code=20=3D>=20"invalid_client",=0A+=09=09error_status=20=3D>=20= 401),=0A+=09"bad=20token=20response:=20client=20authentication=20= failure,=20default=20description=20with=20oauth_client_secret",=0A+=09= expected_stderr=20=3D>=0A+=09=20=20qr/failed=20to=20obtain=20access=20= token:=20provider=20rejected=20the=20oauth_client_secret=20= \(invalid_client\)/=0A+);=0A+$node->connect_fails(=0A+=09connstr(=0A+=09=09= stage=20=3D>=20'token',=0A+=09=09error_code=20=3D>=20"invalid_client",=0A= +=09=09error_status=20=3D>=20401,=0A+=09=09error_desc=20=3D>=20"mutual=20= TLS=20required=20for=20client"),=0A+=09"bad=20token=20response:=20client=20= authentication=20failure,=20provided=20description=20with=20= oauth_client_secret",=0A+=09expected_stderr=20=3D>=0A+=09=20=20qr/failed=20= to=20obtain=20access=20token:=20mutual=20TLS=20required=20for=20client=20= \(invalid_client\)/=0A+);=0A+=0A+#=20Stress=20test:=20make=20sure=20our=20= builtin=20flow=20operates=20correctly=20even=20if=20the=20client=0A+#=20= application=20isn't=20respecting=20PGRES_POLLING_READING/WRITING=20= signals=20returned=0A+#=20from=20PQconnectPoll().=0A+$base_connstr=20=3D=0A= +=20=20"$common_connstr=20port=3D"=20.=20$node->port=20.=20"=20host=3D"=20= .=20$node->host;=0A+my=20@cmd=20=3D=20(=0A+=09"oauth_hook_client",=20= "--no-hook",=20"--stress-async",=0A+=09connstr(stage=20=3D>=20'all',=20= retries=20=3D>=201,=20interval=20=3D>=201));=0A+=0A+note=20"running=20'"=20= .=20join("'=20'",=20@cmd)=20.=20"'";=0A+my=20($stdout,=20$stderr)=20=3D=20= run_command(\@cmd);=0A+=0A+like($stdout,=20qr/connection=20succeeded/,=20= "stress-async:=20stdout=20matches");=0A+unlike($stderr,=20qr/connection=20= to=20database=20failed/,=20"stress-async:=20stderr=20matches");=0A+=0A+#=0A= +#=20This=20section=20of=20tests=20reconfigures=20the=20validator=20= module=20itself,=20rather=20than=0A+#=20the=20OAuth=20server.=0A+#=0A+=0A= +#=20Searching=20the=20logs=20is=20easier=20if=20OAuth=20parameter=20= discovery=20isn't=20cluttering=0A+#=20things=20up;=20hardcode=20the=20= discovery=20URI.=20(Scope=20is=20hardcoded=20to=20empty=20to=20cover=0A= +#=20that=20case=20as=20well.)=0A+$common_connstr=20=3D=0A+=20=20= "dbname=3Dpostgres=20= oauth_issuer=3D$issuer/.well-known/openid-configuration=20oauth_scope=3D''= =20oauth_client_id=3Df02c6361-0635";=0A+=0A+#=20Misbehaving=20validators=20= must=20fail=20shut.=0A+$bgconn->query_safe("ALTER=20SYSTEM=20SET=20= oauth_validator.authn_id=20TO=20''");=0A+$node->reload;=0A+$log_start=20= =3D=0A+=20=20$node->wait_for_log(qr/reloading=20configuration=20files/,=20= $log_start);=0A+=0A+$node->connect_fails(=0A+=09"$common_connstr=20= user=3Dtest",=0A+=09"validator=20must=20set=20authn_id",=0A+=09= expected_stderr=20=3D>=20qr/OAuth=20bearer=20authentication=20failed/,=0A= +=09log_like=20=3D>=20[=0A+=09=09qr/connection=20authenticated:=20= identity=3D""/,=0A+=09=09qr/DETAIL:\s+Validator=20provided=20no=20= identity/,=0A+=09=09qr/FATAL:\s+OAuth=20bearer=20authentication=20= failed/,=0A+=09]);=0A+=0A+#=20Even=20if=20a=20validator=20authenticates=20= the=20user,=20if=20the=20token=20isn't=20considered=0A+#=20valid,=20the=20= connection=20fails.=0A+$bgconn->query_safe(=0A+=09"ALTER=20SYSTEM=20SET=20= oauth_validator.authn_id=20TO=20'test\@example.org'");=0A= +$bgconn->query_safe(=0A+=09"ALTER=20SYSTEM=20SET=20= oauth_validator.authorize_tokens=20TO=20false");=0A+$node->reload;=0A= +$log_start=20=3D=0A+=20=20$node->wait_for_log(qr/reloading=20= configuration=20files/,=20$log_start);=0A+=0A+$node->connect_fails(=0A+=09= "$common_connstr=20user=3Dtest",=0A+=09"validator=20must=20authorize=20= token=20explicitly",=0A+=09expected_stderr=20=3D>=20qr/OAuth=20bearer=20= authentication=20failed/,=0A+=09log_like=20=3D>=20[=0A+=09=09= qr/connection=20authenticated:=20identity=3D"test\@example\.org"/,=0A+=09= =09qr/DETAIL:\s+Validator=20failed=20to=20authorize=20the=20provided=20= token/,=0A+=09=09qr/FATAL:\s+OAuth=20bearer=20authentication=20failed/,=0A= +=09]);=0A+=0A+#=0A+#=20Test=20user=20mapping.=0A+#=0A+=0A+#=20Allow=20= "user@example.com"=20to=20log=20in=20under=20the=20test=20role.=0A= +unlink($node->data_dir=20.=20'/pg_ident.conf');=0A+$node->append_conf(=0A= +=09'pg_ident.conf',=20qq{=0A+oauthmap=09user\@example.com=09test=0A+});=0A= +=0A+#=20test=20and=20testalt=20use=20the=20map;=20testparam=20uses=20= ident=20delegation.=0A+unlink($node->data_dir=20.=20'/pg_hba.conf');=0A= +$node->append_conf(=0A+=09'pg_hba.conf',=20qq{=0A+local=20all=20test=20=20= =20=20=20=20oauth=20issuer=3D"$issuer"=20scope=3D""=20map=3Doauthmap=0A= +local=20all=20testalt=20=20=20oauth=20issuer=3D"$issuer"=20scope=3D""=20= map=3Doauthmap=0A+local=20all=20testparam=20oauth=20issuer=3D"$issuer"=20= scope=3D""=20delegate_ident_mapping=3D1=0A+});=0A+=0A+#=20To=20start,=20= have=20the=20validator=20use=20the=20role=20names=20as=20authn=20IDs.=0A= +$bgconn->query_safe("ALTER=20SYSTEM=20RESET=20= oauth_validator.authn_id");=0A+$bgconn->query_safe("ALTER=20SYSTEM=20= RESET=20oauth_validator.authorize_tokens");=0A+=0A+$node->reload;=0A= +$log_start=20=3D=0A+=20=20$node->wait_for_log(qr/reloading=20= configuration=20files/,=20$log_start);=0A+=0A+#=20The=20test=20and=20= testalt=20roles=20should=20no=20longer=20map=20correctly.=0A= +$node->connect_fails(=0A+=09"$common_connstr=20user=3Dtest",=0A+=09= "mismatched=20username=20map=20(test)",=0A+=09expected_stderr=20=3D>=20= qr/OAuth=20bearer=20authentication=20failed/);=0A+$node->connect_fails(=0A= +=09"$common_connstr=20user=3Dtestalt",=0A+=09"mismatched=20username=20= map=20(testalt)",=0A+=09expected_stderr=20=3D>=20qr/OAuth=20bearer=20= authentication=20failed/);=0A+=0A+#=20Have=20the=20validator=20identify=20= the=20end=20user=20as=20user@example.com.=0A+$bgconn->query_safe(=0A+=09= "ALTER=20SYSTEM=20SET=20oauth_validator.authn_id=20TO=20= 'user\@example.com'");=0A+$node->reload;=0A+$log_start=20=3D=0A+=20=20= $node->wait_for_log(qr/reloading=20configuration=20files/,=20= $log_start);=0A+=0A+#=20Now=20the=20test=20role=20can=20be=20logged=20= into.=20(testalt=20still=20can't=20be=20mapped.)=0A+$node->connect_ok(=0A= +=09"$common_connstr=20user=3Dtest",=0A+=09"matched=20username=20map=20= (test)",=0A+=09expected_stderr=20=3D>=0A+=09=20=20qr@Visit=20= https://example\.com/=20and=20enter=20the=20code:=20postgresuser@);=0A= +$node->connect_fails(=0A+=09"$common_connstr=20user=3Dtestalt",=0A+=09= "mismatched=20username=20map=20(testalt)",=0A+=09expected_stderr=20=3D>=20= qr/OAuth=20bearer=20authentication=20failed/);=0A+=0A+#=20testparam=20= ignores=20the=20map=20entirely.=0A+$node->connect_ok(=0A+=09= "$common_connstr=20user=3Dtestparam",=0A+=09"delegated=20ident=20= (testparam)",=0A+=09expected_stderr=20=3D>=0A+=09=20=20qr@Visit=20= https://example\.com/=20and=20enter=20the=20code:=20postgresuser@);=0A+=0A= +$bgconn->query_safe("ALTER=20SYSTEM=20RESET=20= oauth_validator.authn_id");=0A+$node->reload;=0A+$log_start=20=3D=0A+=20=20= $node->wait_for_log(qr/reloading=20configuration=20files/,=20= $log_start);=0A+=0A+#=0A+#=20Test=20multiple=20validators.=0A+#=0A+=0A= +$node->append_conf('postgresql.conf',=0A+=09"oauth_validator_libraries=20= =3D=20'validator,=20fail_validator'\n");=0A+=0A+#=20With=20multiple=20= validators,=20every=20HBA=20line=20must=20explicitly=20declare=20one.=0A= +my=20$result=20=3D=20$node->restart(fail_ok=20=3D>=201);=0A+is($result,=20= 0,=0A+=09'restart=20fails=20without=20explicit=20validators=20in=20oauth=20= HBA=20entries');=0A+=0A+$log_start=20=3D=20$node->wait_for_log(=0A+=09= qr/authentication=20method=20"oauth"=20requires=20argument=20"validator"=20= to=20be=20set/,=0A+=09$log_start);=0A+=0A+unlink($node->data_dir=20.=20= '/pg_hba.conf');=0A+$node->append_conf(=0A+=09'pg_hba.conf',=20qq{=0A= +local=20all=20test=20=20=20=20oauth=20validator=3Dvalidator=20=20=20=20=20= =20issuer=3D"$issuer"=20=20=20=20=20=20=20=20=20=20=20scope=3D"openid=20= postgres"=0A+local=20all=20testalt=20oauth=20validator=3Dfail_validator=20= issuer=3D"$issuer/.well-known/oauth-authorization-server/alternate"=20= scope=3D"openid=20postgres=20alt"=0A+});=0A+$node->restart;=0A+=0A= +$log_start=20=3D=20$node->wait_for_log(qr/ready=20to=20accept=20= connections/,=20$log_start);=0A+=0A+#=20The=20test=20user=20should=20= work=20as=20before.=0A+$user=20=3D=20"test";=0A+$node->connect_ok(=0A+=09= "user=3D$user=20dbname=3Dpostgres=20oauth_issuer=3D$issuer=20= oauth_client_id=3Df02c6361-0635",=0A+=09"validator=20is=20used=20for=20= $user",=0A+=09expected_stderr=20=3D>=0A+=09=20=20qr@Visit=20= https://example\.com/=20and=20enter=20the=20code:=20postgresuser@,=0A+=09= log_like=20=3D>=20[qr/connection=20authorized/]);=0A+=0A+#=20testalt=20= should=20be=20routed=20through=20the=20fail_validator.=0A+$user=20=3D=20= "testalt";=0A+$node->connect_fails(=0A+=09"user=3D$user=20= dbname=3Dpostgres=20= oauth_issuer=3D$issuer/.well-known/oauth-authorization-server/alternate=20= oauth_client_id=3Df02c6361-0636",=0A+=09"fail_validator=20is=20used=20= for=20$user",=0A+=09expected_stderr=20=3D>=20qr/FATAL:\s+fail_validator:=20= sentinel=20error/);=0A+=0A+$node->stop;=0A+=0A+done_testing();=0Adiff=20= --git=20a/src/test/modules/oauth_validator/t/002_client.pl=20= b/src/test/modules/oauth_validator/t/002_client.pl=0Anew=20file=20mode=20= 100644=0Aindex=2000000000000..95cccf90dd8=0A---=20/dev/null=0A+++=20= b/src/test/modules/oauth_validator/t/002_client.pl=0A@@=20-0,0=20+1,154=20= @@=0A+#=0A+#=20Exercises=20the=20API=20for=20custom=20OAuth=20client=20= flows,=20using=20the=20oauth_hook_client=0A+#=20test=20driver.=0A+#=0A+#=20= Copyright=20(c)=202021-2024,=20PostgreSQL=20Global=20Development=20Group=0A= +#=0A+=0A+use=20strict;=0A+use=20warnings=20FATAL=20=3D>=20'all';=0A+=0A= +use=20JSON::PP=20=20=20=20=20qw(encode_json);=0A+use=20MIME::Base64=20= qw(encode_base64);=0A+use=20PostgreSQL::Test::Cluster;=0A+use=20= PostgreSQL::Test::Utils;=0A+use=20Test::More;=0A+=0A+if=20= (!$ENV{PG_TEST_EXTRA}=20||=20$ENV{PG_TEST_EXTRA}=20!~=20/\boauth\b/)=0A= +{=0A+=09plan=20skip_all=20=3D>=0A+=09=20=20'Potentially=20unsafe=20test=20= oauth=20not=20enabled=20in=20PG_TEST_EXTRA';=0A+}=0A+=0A+#=0A+#=20= Cluster=20Setup=0A+#=0A+=0A+my=20$node=20=3D=20= PostgreSQL::Test::Cluster->new('primary');=0A+$node->init;=0A= +$node->append_conf('postgresql.conf',=20"log_connections=20=3D=20= on\n");=0A+$node->append_conf('postgresql.conf',=0A+=09= "oauth_validator_libraries=20=3D=20'validator'\n");=0A+$node->start;=0A+=0A= +$node->safe_psql('postgres',=20'CREATE=20USER=20test;');=0A+=0A+#=20= These=20tests=20don't=20use=20the=20builtin=20flow,=20and=20we=20don't=20= have=20an=20authorization=0A+#=20server=20running,=20so=20the=20address=20= used=20here=20shouldn't=20matter.=20Use=20an=20invalid=20IP=0A+#=20= address,=20so=20if=20there's=20some=20cascade=20of=20errors=20that=20= causes=20the=20client=20to=0A+#=20attempt=20a=20connection,=20we'll=20= fail=20noisily.=0A+my=20$issuer=20=3D=20"https://256.256.256.256";=0A+my=20= $scope=20=3D=20"openid=20postgres";=0A+=0A+unlink($node->data_dir=20.=20= '/pg_hba.conf');=0A+$node->append_conf(=0A+=09'pg_hba.conf',=20qq{=0A= +local=20all=20test=20oauth=20issuer=3D"$issuer"=20scope=3D"$scope"=0A= +});=0A+$node->reload;=0A+=0A+my=20($log_start,=20$log_end);=0A= +$log_start=20=3D=20$node->wait_for_log(qr/reloading=20configuration=20= files/);=0A+=0A+$ENV{PGOAUTHDEBUG}=20=3D=20"UNSAFE";=0A+=0A+#=0A+#=20= Tests=0A+#=0A+=0A+my=20$user=20=3D=20"test";=0A+my=20$base_connstr=20=3D=20= $node->connstr()=20.=20"=20user=3D$user";=0A+my=20$common_connstr=20=3D=0A= +=20=20"$base_connstr=20oauth_issuer=3D$issuer=20oauth_client_id=3DmyID";=0A= +=0A+sub=20test=0A+{=0A+=09my=20($test_name,=20%params)=20=3D=20@_;=0A+=0A= +=09my=20$flags=20=3D=20[];=0A+=09if=20(defined($params{flags}))=0A+=09{=0A= +=09=09$flags=20=3D=20$params{flags};=0A+=09}=0A+=0A+=09my=20@cmd=20=3D=20= ("oauth_hook_client",=20@{$flags},=20$common_connstr);=0A+=09note=20= "running=20'"=20.=20join("'=20'",=20@cmd)=20.=20"'";=0A+=0A+=09my=20= ($stdout,=20$stderr)=20=3D=20run_command(\@cmd);=0A+=0A+=09if=20= (defined($params{expected_stdout}))=0A+=09{=0A+=09=09like($stdout,=20= $params{expected_stdout},=20"$test_name:=20stdout=20matches");=0A+=09}=0A= +=0A+=09if=20(defined($params{expected_stderr}))=0A+=09{=0A+=09=09= like($stderr,=20$params{expected_stderr},=20"$test_name:=20stderr=20= matches");=0A+=09}=0A+=09else=0A+=09{=0A+=09=09is($stderr,=20"",=20= "$test_name:=20no=20stderr");=0A+=09}=0A+}=0A+=0A+test(=0A+=09"basic=20= synchronous=20hook=20can=20provide=20a=20token",=0A+=09flags=20=3D>=20[=0A= +=09=09"--token",=20"my-token",=0A+=09=09"--expected-uri",=20= "$issuer/.well-known/openid-configuration",=0A+=09=09"--expected-scope",=20= $scope,=0A+=09],=0A+=09expected_stdout=20=3D>=20qr/connection=20= succeeded/);=0A+=0A+$node->log_check("validator=20receives=20correct=20= token",=0A+=09$log_start,=0A+=09log_like=20=3D>=20[=20= qr/oauth_validator:=20token=3D"my-token",=20role=3D"$user"/,=20]);=0A+=0A= +if=20($ENV{with_libcurl}=20ne=20'yes')=0A+{=0A+=09#=20libpq=20should=20= help=20users=20out=20if=20no=20OAuth=20support=20is=20built=20in.=0A+=09= test(=0A+=09=09"fails=20without=20custom=20hook=20installed",=0A+=09=09= flags=20=3D>=20["--no-hook"],=0A+=09=09expected_stderr=20=3D>=0A+=09=09=20= =20qr/no=20custom=20OAuth=20flows=20are=20available,=20and=20libpq=20was=20= not=20built=20with=20libcurl=20support/=0A+=09);=0A+}=0A+=0A+#=20= connect_timeout=20should=20work=20if=20the=20flow=20doesn't=20respond.=0A= +$common_connstr=20=3D=20"$common_connstr=20connect_timeout=3D1";=0A= +test(=0A+=09"connect_timeout=20interrupts=20hung=20client=20flow",=0A+=09= flags=20=3D>=20["--hang-forever"],=0A+=09expected_stderr=20=3D>=20= qr/failed:=20timeout=20expired/);=0A+=0A+#=20Test=20various=20= misbehaviors=20of=20the=20client=20hook.=0A+my=20@cases=20=3D=20(=0A+=09= {=0A+=09=09flag=20=3D>=20"--misbehave=3Dno-hook",=0A+=09=09= expected_error=20=3D>=0A+=09=09=20=20qr/user-defined=20OAuth=20flow=20= provided=20neither=20a=20token=20nor=20an=20async=20callback/,=0A+=09},=0A= +=09{=0A+=09=09flag=20=3D>=20"--misbehave=3Dfail-async",=0A+=09=09= expected_error=20=3D>=20qr/user-defined=20OAuth=20flow=20failed/,=0A+=09= },=0A+=09{=0A+=09=09flag=20=3D>=20"--misbehave=3Dno-token",=0A+=09=09= expected_error=20=3D>=20qr/user-defined=20OAuth=20flow=20did=20not=20= provide=20a=20token/,=0A+=09},=0A+=09{=0A+=09=09flag=20=3D>=20= "--misbehave=3Dno-socket",=0A+=09=09expected_error=20=3D>=0A+=09=09=20=20= qr/user-defined=20OAuth=20flow=20did=20not=20provide=20a=20socket=20for=20= polling/,=0A+=09});=0A+=0A+foreach=20my=20$c=20(@cases)=0A+{=0A+=09test(=0A= +=09=09"hook=20misbehavior:=20$c->{'flag'}",=0A+=09=09flags=20=3D>=20[=20= $c->{'flag'}=20],=0A+=09=09expected_stderr=20=3D>=20= $c->{'expected_error'});=0A+}=0A+=0A+done_testing();=0Adiff=20--git=20= a/src/test/modules/oauth_validator/t/OAuth/Server.pm=20= b/src/test/modules/oauth_validator/t/OAuth/Server.pm=0Anew=20file=20mode=20= 100644=0Aindex=2000000000000..f0f23d1d1a8=0A---=20/dev/null=0A+++=20= b/src/test/modules/oauth_validator/t/OAuth/Server.pm=0A@@=20-0,0=20= +1,140=20@@=0A+=0A+#=20Copyright=20(c)=202024,=20PostgreSQL=20Global=20= Development=20Group=0A+=0A+=3Dpod=0A+=0A+=3Dhead1=20NAME=0A+=0A= +OAuth::Server=20-=20runs=20a=20mock=20OAuth=20authorization=20server=20= for=20testing=0A+=0A+=3Dhead1=20SYNOPSIS=0A+=0A+=20=20use=20= OAuth::Server;=0A+=0A+=20=20my=20$server=20=3D=20OAuth::Server->new();=0A= +=20=20$server->run;=0A+=0A+=20=20my=20$port=20=3D=20$server->port;=0A+=20= =20my=20$issuer=20=3D=20"http://localhost:$port";=0A+=0A+=20=20#=20test=20= against=20$issuer...=0A+=0A+=20=20$server->stop;=0A+=0A+=3Dhead1=20= DESCRIPTION=0A+=0A+This=20is=20glue=20API=20between=20the=20Perl=20tests=20= and=20the=20Python=20authorization=20server=0A+daemon=20implemented=20in=20= t/oauth_server.py.=20(Python=20has=20a=20fairly=20usable=20HTTP=20server=0A= +in=20its=20standard=20library,=20so=20the=20implementation=20was=20= ported=20from=20Perl.)=0A+=0A+This=20authorization=20server=20does=20not=20= use=20TLS=20(it=20implements=20a=20nonstandard,=20unsafe=0A+issuer=20at=20= "http://localhost:"),=20so=20libpq=20in=20particular=20will=20need=20= to=20set=0A+PGOAUTHDEBUG=3DUNSAFE=20to=20be=20able=20to=20talk=20to=20= it.=0A+=0A+=3Dcut=0A+=0A+package=20OAuth::Server;=0A+=0A+use=20warnings;=0A= +use=20strict;=0A+use=20Scalar::Util;=0A+use=20Test::More;=0A+=0A+=3Dpod=0A= +=0A+=3Dhead1=20METHODS=0A+=0A+=3Dover=0A+=0A+=3Ditem=20= SSL::Server->new()=0A+=0A+Create=20a=20new=20OAuth=20Server=20object.=0A= +=0A+=3Dcut=0A+=0A+sub=20new=0A+{=0A+=09my=20$class=20=3D=20shift;=0A+=0A= +=09my=20$self=20=3D=20{};=0A+=09bless($self,=20$class);=0A+=0A+=09= return=20$self;=0A+}=0A+=0A+=3Dpod=0A+=0A+=3Ditem=20$server->port()=0A+=0A= +Returns=20the=20port=20in=20use=20by=20the=20server.=0A+=0A+=3Dcut=0A+=0A= +sub=20port=0A+{=0A+=09my=20$self=20=3D=20shift;=0A+=0A+=09return=20= $self->{'port'};=0A+}=0A+=0A+=3Dpod=0A+=0A+=3Ditem=20$server->run()=0A+=0A= +Runs=20the=20authorization=20server=20daemon=20in=20t/oauth_server.py.=0A= +=0A+=3Dcut=0A+=0A+sub=20run=0A+{=0A+=09my=20$self=20=3D=20shift;=0A+=09= my=20$port;=0A+=0A+=09my=20$pid=20=3D=20open(my=20$read_fh,=20"-|",=20= $ENV{PYTHON},=20"t/oauth_server.py")=0A+=09=20=20or=20die=20"failed=20to=20= start=20OAuth=20server:=20$!";=0A+=0A+=09#=20Get=20the=20port=20number=20= from=20the=20daemon.=20It=20closes=20stdout=20afterwards;=20that=20way=0A= +=09#=20we=20can=20slurp=20in=20the=20entire=20contents=20here=20rather=20= than=20worrying=20about=20the=0A+=09#=20number=20of=20bytes=20to=20read.=0A= +=09$port=20=3D=20do=20{=20local=20$/=20=3D=20undef;=20<$read_fh>=20}=0A= +=09=20=20//=20die=20"failed=20to=20read=20port=20number:=20$!";=0A+=09= chomp=20$port;=0A+=09die=20"server=20did=20not=20advertise=20a=20valid=20= port"=0A+=09=20=20unless=20Scalar::Util::looks_like_number($port);=0A+=0A= +=09$self->{'pid'}=20=3D=20$pid;=0A+=09$self->{'port'}=20=3D=20$port;=0A= +=09$self->{'child'}=20=3D=20$read_fh;=0A+=0A+=09note("OAuth=20provider=20= (PID=20$pid)=20is=20listening=20on=20port=20$port\n");=0A+}=0A+=0A+=3Dpod=0A= +=0A+=3Ditem=20$server->stop()=0A+=0A+Sends=20SIGTERM=20to=20the=20= authorization=20server=20and=20waits=20for=20it=20to=20exit.=0A+=0A+=3Dcut= =0A+=0A+sub=20stop=0A+{=0A+=09my=20$self=20=3D=20shift;=0A+=0A+=09= note("Sending=20SIGTERM=20to=20OAuth=20provider=20PID:=20= $self->{'pid'}\n");=0A+=0A+=09kill(15,=20$self->{'pid'});=0A+=09= $self->{'pid'}=20=3D=20undef;=0A+=0A+=09#=20Closing=20the=20popen()=20= handle=20waits=20for=20the=20process=20to=20exit.=0A+=09= close($self->{'child'});=0A+=09$self->{'child'}=20=3D=20undef;=0A+}=0A+=0A= +=3Dpod=0A+=0A+=3Dback=0A+=0A+=3Dcut=0A+=0A+1;=0Adiff=20--git=20= a/src/test/modules/oauth_validator/t/oauth_server.py=20= b/src/test/modules/oauth_validator/t/oauth_server.py=0Anew=20file=20mode=20= 100755=0Aindex=2000000000000..4faf3323d38=0A---=20/dev/null=0A+++=20= b/src/test/modules/oauth_validator/t/oauth_server.py=0A@@=20-0,0=20= +1,391=20@@=0A+#!=20/usr/bin/env=20python3=0A+#=0A+#=20A=20mock=20OAuth=20= authorization=20server,=20designed=20to=20be=20invoked=20from=0A+#=20= OAuth/Server.pm.=20This=20listens=20on=20an=20ephemeral=20port=20number=20= (printed=20to=20stdout=0A+#=20so=20that=20the=20Perl=20tests=20can=20= contact=20it)=20and=20runs=20as=20a=20daemon=20until=20it=20is=0A+#=20= signaled.=0A+#=0A+=0A+import=20base64=0A+import=20http.server=0A+import=20= json=0A+import=20os=0A+import=20sys=0A+import=20time=0A+import=20= urllib.parse=0A+from=20collections=20import=20defaultdict=0A+=0A+=0A= +class=20OAuthHandler(http.server.BaseHTTPRequestHandler):=0A+=20=20=20=20= """=0A+=20=20=20=20Core=20implementation=20of=20the=20authorization=20= server.=20The=20API=20is=0A+=20=20=20=20inheritance-based,=20with=20= entry=20points=20at=20do_GET()=20and=20do_POST().=20See=20the=0A+=20=20=20= =20documentation=20for=20BaseHTTPRequestHandler.=0A+=20=20=20=20"""=0A+=0A= +=20=20=20=20JsonObject=20=3D=20dict[str,=20object]=20=20#=20TypeAlias=20= is=20not=20available=20until=203.10=0A+=0A+=20=20=20=20def=20= _check_issuer(self):=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20=20=20=20= =20=20Switches=20the=20behavior=20of=20the=20provider=20depending=20on=20= the=20issuer=20URI.=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20=20=20=20=20= =20self._alt_issuer=20=3D=20(=0A+=20=20=20=20=20=20=20=20=20=20=20=20= self.path.startswith("/alternate/")=0A+=20=20=20=20=20=20=20=20=20=20=20=20= or=20self.path=20=3D=3D=20= "/.well-known/oauth-authorization-server/alternate"=0A+=20=20=20=20=20=20= =20=20)=0A+=20=20=20=20=20=20=20=20self._parameterized=20=3D=20= self.path.startswith("/param/")=0A+=0A+=20=20=20=20=20=20=20=20if=20= self._alt_issuer:=0A+=20=20=20=20=20=20=20=20=20=20=20=20#=20The=20= /alternate=20issuer=20uses=20IETF-style=20.well-known=20URIs.=0A+=20=20=20= =20=20=20=20=20=20=20=20=20if=20self.path.startswith("/.well-known/"):=0A= +=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20self.path=20=3D=20= self.path.removesuffix("/alternate")=0A+=20=20=20=20=20=20=20=20=20=20=20= =20else:=0A+=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20self.path=20= =3D=20self.path.removeprefix("/alternate")=0A+=20=20=20=20=20=20=20=20= elif=20self._parameterized:=0A+=20=20=20=20=20=20=20=20=20=20=20=20= self.path=20=3D=20self.path.removeprefix("/param")=0A+=0A+=20=20=20=20= def=20_check_authn(self):=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20=20=20= =20=20=20Checks=20the=20expected=20value=20of=20the=20Authorization=20= header,=20if=20any.=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20=20=20=20=20= =20secret=20=3D=20self._get_param("expected_secret",=20None)=0A+=20=20=20= =20=20=20=20=20if=20secret=20is=20None:=0A+=20=20=20=20=20=20=20=20=20=20= =20=20return=0A+=0A+=20=20=20=20=20=20=20=20assert=20"Authorization"=20= in=20self.headers=0A+=20=20=20=20=20=20=20=20method,=20creds=20=3D=20= self.headers["Authorization"].split()=0A+=0A+=20=20=20=20=20=20=20=20if=20= method=20!=3D=20"Basic":=0A+=20=20=20=20=20=20=20=20=20=20=20=20raise=20= RuntimeError(f"client=20used=20{method}=20auth;=20expected=20Basic")=0A+=0A= +=20=20=20=20=20=20=20=20username=20=3D=20= urllib.parse.quote_plus(self.client_id)=0A+=20=20=20=20=20=20=20=20= password=20=3D=20urllib.parse.quote_plus(secret)=0A+=20=20=20=20=20=20=20= =20expected_creds=20=3D=20f"{username}:{password}"=0A+=0A+=20=20=20=20=20= =20=20=20if=20creds.encode()=20!=3D=20= base64.b64encode(expected_creds.encode()):=0A+=20=20=20=20=20=20=20=20=20= =20=20=20raise=20RuntimeError(=0A+=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20f"client=20sent=20'{creds}';=20expected=20= b64encode('{expected_creds}')"=0A+=20=20=20=20=20=20=20=20=20=20=20=20)=0A= +=0A+=20=20=20=20def=20do_GET(self):=0A+=20=20=20=20=20=20=20=20= self._response_code=20=3D=20200=0A+=20=20=20=20=20=20=20=20= self._check_issuer()=0A+=0A+=20=20=20=20=20=20=20=20config_path=20=3D=20= "/.well-known/openid-configuration"=0A+=20=20=20=20=20=20=20=20if=20= self._alt_issuer:=0A+=20=20=20=20=20=20=20=20=20=20=20=20config_path=20=3D= =20"/.well-known/oauth-authorization-server"=0A+=0A+=20=20=20=20=20=20=20= =20if=20self.path=20=3D=3D=20config_path:=0A+=20=20=20=20=20=20=20=20=20=20= =20=20resp=20=3D=20self.config()=0A+=20=20=20=20=20=20=20=20else:=0A+=20=20= =20=20=20=20=20=20=20=20=20=20self.send_error(404,=20"Not=20Found")=0A+=20= =20=20=20=20=20=20=20=20=20=20=20return=0A+=0A+=20=20=20=20=20=20=20=20= self._send_json(resp)=0A+=0A+=20=20=20=20def=20_parse_params(self)=20->=20= dict[str,=20str]:=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20=20=20=20=20= =20Parses=20apart=20the=20form-urlencoded=20request=20body=20and=20= returns=20the=20resulting=0A+=20=20=20=20=20=20=20=20dict.=20For=20use=20= by=20do_POST().=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20=20=20=20=20=20= size=20=3D=20int(self.headers["Content-Length"])=0A+=20=20=20=20=20=20=20= =20form=20=3D=20self.rfile.read(size)=0A+=0A+=20=20=20=20=20=20=20=20= assert=20self.headers["Content-Type"]=20=3D=3D=20= "application/x-www-form-urlencoded"=0A+=20=20=20=20=20=20=20=20return=20= urllib.parse.parse_qs(=0A+=20=20=20=20=20=20=20=20=20=20=20=20= form.decode("utf-8"),=0A+=20=20=20=20=20=20=20=20=20=20=20=20= strict_parsing=3DTrue,=0A+=20=20=20=20=20=20=20=20=20=20=20=20= keep_blank_values=3DTrue,=0A+=20=20=20=20=20=20=20=20=20=20=20=20= encoding=3D"utf-8",=0A+=20=20=20=20=20=20=20=20=20=20=20=20= errors=3D"strict",=0A+=20=20=20=20=20=20=20=20)=0A+=0A+=20=20=20=20= @property=0A+=20=20=20=20def=20client_id(self)=20->=20str:=0A+=20=20=20=20= =20=20=20=20"""=0A+=20=20=20=20=20=20=20=20Returns=20the=20client_id=20= sent=20in=20the=20POST=20body=20or=20the=20Authorization=20header.=0A+=20= =20=20=20=20=20=20=20self._parse_params()=20must=20have=20been=20called=20= first.=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20=20=20=20=20=20if=20= "client_id"=20in=20self._params:=0A+=20=20=20=20=20=20=20=20=20=20=20=20= return=20self._params["client_id"][0]=0A+=0A+=20=20=20=20=20=20=20=20if=20= "Authorization"=20not=20in=20self.headers:=0A+=20=20=20=20=20=20=20=20=20= =20=20=20raise=20RuntimeError("client=20did=20not=20send=20any=20= client_id")=0A+=0A+=20=20=20=20=20=20=20=20_,=20creds=20=3D=20= self.headers["Authorization"].split()=0A+=0A+=20=20=20=20=20=20=20=20= decoded=20=3D=20base64.b64decode(creds).decode("utf-8")=0A+=20=20=20=20=20= =20=20=20username,=20_=20=3D=20decoded.split(":",=201)=0A+=0A+=20=20=20=20= =20=20=20=20return=20urllib.parse.unquote_plus(username)=0A+=0A+=20=20=20= =20def=20do_POST(self):=0A+=20=20=20=20=20=20=20=20self._response_code=20= =3D=20200=0A+=20=20=20=20=20=20=20=20self._check_issuer()=0A+=0A+=20=20=20= =20=20=20=20=20self._params=20=3D=20self._parse_params()=0A+=20=20=20=20=20= =20=20=20if=20self._parameterized:=0A+=20=20=20=20=20=20=20=20=20=20=20=20= #=20Pull=20encoded=20test=20parameters=20out=20of=20the=20peer's=20= client_id=20field.=0A+=20=20=20=20=20=20=20=20=20=20=20=20#=20This=20is=20= expected=20to=20be=20Base64-encoded=20JSON.=0A+=20=20=20=20=20=20=20=20=20= =20=20=20js=20=3D=20base64.b64decode(self.client_id)=0A+=20=20=20=20=20=20= =20=20=20=20=20=20self._test_params=20=3D=20json.loads(js)=0A+=0A+=20=20=20= =20=20=20=20=20self._check_authn()=0A+=0A+=20=20=20=20=20=20=20=20if=20= self.path=20=3D=3D=20"/authorize":=0A+=20=20=20=20=20=20=20=20=20=20=20=20= resp=20=3D=20self.authorization()=0A+=20=20=20=20=20=20=20=20elif=20= self.path=20=3D=3D=20"/token":=0A+=20=20=20=20=20=20=20=20=20=20=20=20= resp=20=3D=20self.token()=0A+=20=20=20=20=20=20=20=20else:=0A+=20=20=20=20= =20=20=20=20=20=20=20=20self.send_error(404)=0A+=20=20=20=20=20=20=20=20=20= =20=20=20return=0A+=0A+=20=20=20=20=20=20=20=20self._send_json(resp)=0A+=0A= +=20=20=20=20def=20_should_modify(self)=20->=20bool:=0A+=20=20=20=20=20=20= =20=20"""=0A+=20=20=20=20=20=20=20=20Returns=20True=20if=20the=20client=20= has=20requested=20a=20modification=20to=20this=20stage=20of=0A+=20=20=20=20= =20=20=20=20the=20exchange.=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20=20= =20=20=20=20if=20not=20hasattr(self,=20"_test_params"):=0A+=20=20=20=20=20= =20=20=20=20=20=20=20return=20False=0A+=0A+=20=20=20=20=20=20=20=20stage=20= =3D=20self._test_params.get("stage")=0A+=0A+=20=20=20=20=20=20=20=20= return=20(=0A+=20=20=20=20=20=20=20=20=20=20=20=20stage=20=3D=3D=20"all"=0A= +=20=20=20=20=20=20=20=20=20=20=20=20or=20(=0A+=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20stage=20=3D=3D=20"discovery"=0A+=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20and=20self.path=20=3D=3D=20= "/.well-known/openid-configuration"=0A+=20=20=20=20=20=20=20=20=20=20=20=20= )=0A+=20=20=20=20=20=20=20=20=20=20=20=20or=20(stage=20=3D=3D=20"device"=20= and=20self.path=20=3D=3D=20"/authorize")=0A+=20=20=20=20=20=20=20=20=20=20= =20=20or=20(stage=20=3D=3D=20"token"=20and=20self.path=20=3D=3D=20= "/token")=0A+=20=20=20=20=20=20=20=20)=0A+=0A+=20=20=20=20def=20= _get_param(self,=20name,=20default):=0A+=20=20=20=20=20=20=20=20"""=0A+=20= =20=20=20=20=20=20=20If=20the=20client=20has=20requested=20a=20= modification=20to=20this=20stage=20(see=0A+=20=20=20=20=20=20=20=20= _should_modify()),=20this=20method=20searches=20the=20provided=20test=20= parameters=20for=0A+=20=20=20=20=20=20=20=20a=20key=20of=20the=20given=20= name,=20and=20returns=20it=20if=20found.=20Otherwise=20the=20provided=0A= +=20=20=20=20=20=20=20=20default=20is=20returned.=0A+=20=20=20=20=20=20=20= =20"""=0A+=20=20=20=20=20=20=20=20if=20self._should_modify()=20and=20= name=20in=20self._test_params:=0A+=20=20=20=20=20=20=20=20=20=20=20=20= return=20self._test_params[name]=0A+=0A+=20=20=20=20=20=20=20=20return=20= default=0A+=0A+=20=20=20=20@property=0A+=20=20=20=20def=20= _content_type(self)=20->=20str:=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20= =20=20=20=20=20Returns=20"application/json"=20unless=20the=20test=20has=20= requested=20something=0A+=20=20=20=20=20=20=20=20different.=0A+=20=20=20=20= =20=20=20=20"""=0A+=20=20=20=20=20=20=20=20return=20= self._get_param("content_type",=20"application/json")=0A+=0A+=20=20=20=20= @property=0A+=20=20=20=20def=20_interval(self)=20->=20int:=0A+=20=20=20=20= =20=20=20=20"""=0A+=20=20=20=20=20=20=20=20Returns=200=20unless=20the=20= test=20has=20requested=20something=20different.=0A+=20=20=20=20=20=20=20=20= """=0A+=20=20=20=20=20=20=20=20return=20self._get_param("interval",=200)=0A= +=0A+=20=20=20=20@property=0A+=20=20=20=20def=20_retry_code(self)=20->=20= str:=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20=20=20=20=20=20Returns=20= "authorization_pending"=20unless=20the=20test=20has=20requested=20= something=0A+=20=20=20=20=20=20=20=20different.=0A+=20=20=20=20=20=20=20=20= """=0A+=20=20=20=20=20=20=20=20return=20self._get_param("retry_code",=20= "authorization_pending")=0A+=0A+=20=20=20=20@property=0A+=20=20=20=20def=20= _uri_spelling(self)=20->=20str:=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20= =20=20=20=20=20Returns=20"verification_uri"=20unless=20the=20test=20has=20= requested=20something=0A+=20=20=20=20=20=20=20=20different.=0A+=20=20=20=20= =20=20=20=20"""=0A+=20=20=20=20=20=20=20=20return=20= self._get_param("uri_spelling",=20"verification_uri")=0A+=0A+=20=20=20=20= @property=0A+=20=20=20=20def=20_response_padding(self):=0A+=20=20=20=20=20= =20=20=20"""=0A+=20=20=20=20=20=20=20=20If=20the=20huge_response=20test=20= parameter=20is=20set=20to=20True,=20returns=20a=20dict=0A+=20=20=20=20=20= =20=20=20containing=20a=20gigantic=20string=20value,=20which=20can=20= then=20be=20folded=20into=20a=20JSON=0A+=20=20=20=20=20=20=20=20= response.=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20=20=20=20=20=20if=20= not=20self._get_param("huge_response",=20False):=0A+=20=20=20=20=20=20=20= =20=20=20=20=20return=20dict()=0A+=0A+=20=20=20=20=20=20=20=20return=20= {"_pad_":=20"x"=20*=201024=20*=201024}=0A+=0A+=20=20=20=20@property=0A+=20= =20=20=20def=20_access_token(self):=0A+=20=20=20=20=20=20=20=20"""=0A+=20= =20=20=20=20=20=20=20The=20actual=20Bearer=20token=20sent=20back=20to=20= the=20client=20on=20success.=20Tests=20may=0A+=20=20=20=20=20=20=20=20= override=20this=20with=20the=20"token"=20test=20parameter.=0A+=20=20=20=20= =20=20=20=20"""=0A+=20=20=20=20=20=20=20=20token=20=3D=20= self._get_param("token",=20None)=0A+=20=20=20=20=20=20=20=20if=20token=20= is=20not=20None:=0A+=20=20=20=20=20=20=20=20=20=20=20=20return=20token=0A= +=0A+=20=20=20=20=20=20=20=20token=20=3D=20"9243959234"=0A+=20=20=20=20=20= =20=20=20if=20self._alt_issuer:=0A+=20=20=20=20=20=20=20=20=20=20=20=20= token=20+=3D=20"-alt"=0A+=0A+=20=20=20=20=20=20=20=20return=20token=0A+=0A= +=20=20=20=20def=20_send_json(self,=20js:=20JsonObject)=20->=20None:=0A+=20= =20=20=20=20=20=20=20"""=0A+=20=20=20=20=20=20=20=20Sends=20the=20= provided=20JSON=20dict=20as=20an=20application/json=20response.=0A+=20=20= =20=20=20=20=20=20self._response_code=20can=20be=20modified=20to=20send=20= JSON=20error=20responses.=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20=20=20= =20=20=20resp=20=3D=20json.dumps(js).encode("ascii")=0A+=20=20=20=20=20=20= =20=20self.log_message("sending=20JSON=20response:=20%s",=20resp)=0A+=0A= +=20=20=20=20=20=20=20=20self.send_response(self._response_code)=0A+=20=20= =20=20=20=20=20=20self.send_header("Content-Type",=20self._content_type)=0A= +=20=20=20=20=20=20=20=20self.send_header("Content-Length",=20= str(len(resp)))=0A+=20=20=20=20=20=20=20=20self.end_headers()=0A+=0A+=20=20= =20=20=20=20=20=20self.wfile.write(resp)=0A+=0A+=20=20=20=20def=20= config(self)=20->=20JsonObject:=0A+=20=20=20=20=20=20=20=20port=20=3D=20= self.server.socket.getsockname()[1]=0A+=0A+=20=20=20=20=20=20=20=20= issuer=20=3D=20f"http://localhost:{port}"=0A+=20=20=20=20=20=20=20=20if=20= self._alt_issuer:=0A+=20=20=20=20=20=20=20=20=20=20=20=20issuer=20+=3D=20= "/alternate"=0A+=20=20=20=20=20=20=20=20elif=20self._parameterized:=0A+=20= =20=20=20=20=20=20=20=20=20=20=20issuer=20+=3D=20"/param"=0A+=0A+=20=20=20= =20=20=20=20=20return=20{=0A+=20=20=20=20=20=20=20=20=20=20=20=20= "issuer":=20issuer,=0A+=20=20=20=20=20=20=20=20=20=20=20=20= "token_endpoint":=20issuer=20+=20"/token",=0A+=20=20=20=20=20=20=20=20=20= =20=20=20"device_authorization_endpoint":=20issuer=20+=20"/authorize",=0A= +=20=20=20=20=20=20=20=20=20=20=20=20"response_types_supported":=20= ["token"],=0A+=20=20=20=20=20=20=20=20=20=20=20=20= "subject_types_supported":=20["public"],=0A+=20=20=20=20=20=20=20=20=20=20= =20=20"id_token_signing_alg_values_supported":=20["RS256"],=0A+=20=20=20=20= =20=20=20=20=20=20=20=20"grant_types_supported":=20[=0A+=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20"authorization_code",=0A+=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20= "urn:ietf:params:oauth:grant-type:device_code",=0A+=20=20=20=20=20=20=20=20= =20=20=20=20],=0A+=20=20=20=20=20=20=20=20}=0A+=0A+=20=20=20=20@property=0A= +=20=20=20=20def=20_token_state(self):=0A+=20=20=20=20=20=20=20=20"""=0A= +=20=20=20=20=20=20=20=20A=20cached=20_TokenState=20object=20for=20the=20= connected=20client=20(as=20determined=20by=0A+=20=20=20=20=20=20=20=20= the=20request's=20client_id),=20or=20a=20new=20one=20if=20it=20doesn't=20= already=20exist.=0A+=0A+=20=20=20=20=20=20=20=20This=20relies=20on=20the=20= existence=20of=20a=20defaultdict=20attached=20to=20the=20server;=0A+=20=20= =20=20=20=20=20=20see=20main()=20below.=0A+=20=20=20=20=20=20=20=20"""=0A= +=20=20=20=20=20=20=20=20return=20= self.server.token_state[self.client_id]=0A+=0A+=20=20=20=20def=20= _remove_token_state(self):=0A+=20=20=20=20=20=20=20=20"""=0A+=20=20=20=20= =20=20=20=20Removes=20any=20cached=20_TokenState=20for=20the=20current=20= client_id.=20Call=20this=0A+=20=20=20=20=20=20=20=20after=20the=20token=20= exchange=20ends=20to=20get=20rid=20of=20unnecessary=20state.=0A+=20=20=20= =20=20=20=20=20"""=0A+=20=20=20=20=20=20=20=20if=20self.client_id=20in=20= self.server.token_state:=0A+=20=20=20=20=20=20=20=20=20=20=20=20del=20= self.server.token_state[self.client_id]=0A+=0A+=20=20=20=20def=20= authorization(self)=20->=20JsonObject:=0A+=20=20=20=20=20=20=20=20uri=20= =3D=20"https://example.com/"=0A+=20=20=20=20=20=20=20=20if=20= self._alt_issuer:=0A+=20=20=20=20=20=20=20=20=20=20=20=20uri=20=3D=20= "https://example.org/"=0A+=0A+=20=20=20=20=20=20=20=20resp=20=3D=20{=0A+=20= =20=20=20=20=20=20=20=20=20=20=20"device_code":=20"postgres",=0A+=20=20=20= =20=20=20=20=20=20=20=20=20"user_code":=20"postgresuser",=0A+=20=20=20=20= =20=20=20=20=20=20=20=20self._uri_spelling:=20uri,=0A+=20=20=20=20=20=20=20= =20=20=20=20=20"expires_in":=205,=0A+=20=20=20=20=20=20=20=20=20=20=20=20= **self._response_padding,=0A+=20=20=20=20=20=20=20=20}=0A+=0A+=20=20=20=20= =20=20=20=20interval=20=3D=20self._interval=0A+=20=20=20=20=20=20=20=20= if=20interval=20is=20not=20None:=0A+=20=20=20=20=20=20=20=20=20=20=20=20= resp["interval"]=20=3D=20interval=0A+=20=20=20=20=20=20=20=20=20=20=20=20= self._token_state.min_delay=20=3D=20interval=0A+=20=20=20=20=20=20=20=20= else:=0A+=20=20=20=20=20=20=20=20=20=20=20=20self._token_state.min_delay=20= =3D=205=20=20#=20default=0A+=0A+=20=20=20=20=20=20=20=20#=20Check=20the=20= scope.=0A+=20=20=20=20=20=20=20=20if=20"scope"=20in=20self._params:=0A+=20= =20=20=20=20=20=20=20=20=20=20=20assert=20self._params["scope"][0],=20= "empty=20scopes=20should=20be=20omitted"=0A+=0A+=20=20=20=20=20=20=20=20= return=20resp=0A+=0A+=20=20=20=20def=20token(self)=20->=20JsonObject:=0A= +=20=20=20=20=20=20=20=20if=20err=20:=3D=20self._get_param("error_code",=20= None):=0A+=20=20=20=20=20=20=20=20=20=20=20=20self._response_code=20=3D=20= self._get_param("error_status",=20400)=0A+=0A+=20=20=20=20=20=20=20=20=20= =20=20=20resp=20=3D=20{"error":=20err}=0A+=20=20=20=20=20=20=20=20=20=20=20= =20if=20desc=20:=3D=20self._get_param("error_desc",=20""):=0A+=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20resp["error_description"]=20=3D=20= desc=0A+=0A+=20=20=20=20=20=20=20=20=20=20=20=20return=20resp=0A+=0A+=20=20= =20=20=20=20=20=20if=20self._should_modify()=20and=20"retries"=20in=20= self._test_params:=0A+=20=20=20=20=20=20=20=20=20=20=20=20retries=20=3D=20= self._test_params["retries"]=0A+=0A+=20=20=20=20=20=20=20=20=20=20=20=20= #=20Check=20to=20make=20sure=20the=20token=20interval=20is=20being=20= respected.=0A+=20=20=20=20=20=20=20=20=20=20=20=20now=20=3D=20= time.monotonic()=0A+=20=20=20=20=20=20=20=20=20=20=20=20if=20= self._token_state.last_try=20is=20not=20None:=0A+=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20delay=20=3D=20now=20-=20= self._token_state.last_try=0A+=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20assert=20(=0A+=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20delay=20>=20self._token_state.min_delay=0A+=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20),=20f"client=20waited=20only=20{delay}=20seconds=20= between=20token=20requests=20(expected=20{self._token_state.min_delay})"=0A= +=0A+=20=20=20=20=20=20=20=20=20=20=20=20self._token_state.last_try=20=3D=20= now=0A+=0A+=20=20=20=20=20=20=20=20=20=20=20=20#=20If=20we=20haven't=20= reached=20the=20required=20number=20of=20retries=20yet,=20return=20a=0A+=20= =20=20=20=20=20=20=20=20=20=20=20#=20"pending"=20response.=0A+=20=20=20=20= =20=20=20=20=20=20=20=20if=20self._token_state.retries=20<=20retries:=0A= +=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= self._token_state.retries=20+=3D=201=0A+=0A+=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20self._response_code=20=3D=20400=0A+=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20return=20{"error":=20self._retry_code}=0A+=0A= +=20=20=20=20=20=20=20=20#=20Clean=20up=20any=20retry=20tracking=20state=20= now=20that=20the=20exchange=20is=20ending.=0A+=20=20=20=20=20=20=20=20= self._remove_token_state()=0A+=0A+=20=20=20=20=20=20=20=20return=20{=0A+=20= =20=20=20=20=20=20=20=20=20=20=20"access_token":=20self._access_token,=0A= +=20=20=20=20=20=20=20=20=20=20=20=20"token_type":=20"bearer",=0A+=20=20=20= =20=20=20=20=20=20=20=20=20**self._response_padding,=0A+=20=20=20=20=20=20= =20=20}=0A+=0A+=0A+def=20main():=0A+=20=20=20=20"""=0A+=20=20=20=20= Starts=20the=20authorization=20server=20on=20localhost.=20The=20= ephemeral=20port=20in=20use=20will=0A+=20=20=20=20be=20printed=20to=20= stdout.=0A+=20=20=20=20"""=0A+=0A+=20=20=20=20s=20=3D=20= http.server.HTTPServer(("127.0.0.1",=200),=20OAuthHandler)=0A+=0A+=20=20=20= =20#=20Attach=20a=20"cache"=20dictionary=20to=20the=20server=20to=20= allow=20the=20OAuthHandlers=20to=0A+=20=20=20=20#=20track=20state=20= across=20token=20requests.=20The=20use=20of=20defaultdict=20ensures=20= that=20new=0A+=20=20=20=20#=20entries=20will=20be=20created=20= automatically.=0A+=20=20=20=20class=20_TokenState:=0A+=20=20=20=20=20=20=20= =20retries=20=3D=200=0A+=20=20=20=20=20=20=20=20min_delay=20=3D=20None=0A= +=20=20=20=20=20=20=20=20last_try=20=3D=20None=0A+=0A+=20=20=20=20= s.token_state=20=3D=20defaultdict(_TokenState)=0A+=0A+=20=20=20=20#=20= Give=20the=20parent=20the=20port=20number=20to=20contact=20(this=20is=20= also=20the=20signal=20that=0A+=20=20=20=20#=20we're=20ready=20to=20= receive=20requests).=0A+=20=20=20=20port=20=3D=20= s.socket.getsockname()[1]=0A+=20=20=20=20print(port)=0A+=0A+=20=20=20=20= #=20stdout=20is=20closed=20to=20allow=20the=20parent=20to=20just=20"read=20= to=20the=20end".=0A+=20=20=20=20stdout=20=3D=20sys.stdout.fileno()=0A+=20= =20=20=20sys.stdout.close()=0A+=20=20=20=20os.close(stdout)=0A+=0A+=20=20= =20=20s.serve_forever()=20=20#=20we=20expect=20our=20parent=20to=20send=20= a=20termination=20signal=0A+=0A+=0A+if=20__name__=20=3D=3D=20"__main__":=0A= +=20=20=20=20main()=0Adiff=20--git=20= a/src/test/modules/oauth_validator/validator.c=20= b/src/test/modules/oauth_validator/validator.c=0Anew=20file=20mode=20= 100644=0Aindex=2000000000000..ef9bbb2866f=0A---=20/dev/null=0A+++=20= b/src/test/modules/oauth_validator/validator.c=0A@@=20-0,0=20+1,135=20@@=0A= +/*-----------------------------------------------------------------------= --=0A+=20*=0A+=20*=20validator.c=0A+=20*=09=20=20Test=20module=20for=20= serverside=20OAuth=20token=20validation=20callbacks=0A+=20*=0A+=20*=20= Portions=20Copyright=20(c)=201996-2024,=20PostgreSQL=20Global=20= Development=20Group=0A+=20*=20Portions=20Copyright=20(c)=201994,=20= Regents=20of=20the=20University=20of=20California=0A+=20*=0A+=20*=20= src/test/modules/oauth_validator/validator.c=0A+=20*=0A+=20= *-------------------------------------------------------------------------= =0A+=20*/=0A+=0A+#include=20"postgres.h"=0A+=0A+#include=20"fmgr.h"=0A= +#include=20"libpq/oauth.h"=0A+#include=20"miscadmin.h"=0A+#include=20= "utils/guc.h"=0A+#include=20"utils/memutils.h"=0A+=0A+PG_MODULE_MAGIC;=0A= +=0A+static=20void=20validator_startup(ValidatorModuleState=20*state);=0A= +static=20void=20validator_shutdown(ValidatorModuleState=20*state);=0A= +static=20ValidatorModuleResult=20*validate_token(ValidatorModuleState=20= *state,=0A+=09=09=09=09=09=09=09=09=09=09=09=20const=20char=20*token,=0A= +=09=09=09=09=09=09=09=09=09=09=09=20const=20char=20*role);=0A+=0A+/*=20= Callback=20implementations=20(exercise=20all=20three)=20*/=0A+static=20= const=20OAuthValidatorCallbacks=20validator_callbacks=20=3D=20{=0A+=09= .startup_cb=20=3D=20validator_startup,=0A+=09.shutdown_cb=20=3D=20= validator_shutdown,=0A+=09.validate_cb=20=3D=20validate_token=0A+};=0A+=0A= +/*=20GUCs=20*/=0A+static=20char=20*authn_id=20=3D=20NULL;=0A+static=20= bool=20authorize_tokens=20=3D=20true;=0A+=0A+/*---=0A+=20*=20Extension=20= entry=20point.=20Sets=20up=20GUCs=20for=20use=20by=20tests:=0A+=20*=0A+=20= *=20-=20oauth_validator.authn_id=09Sets=20the=20user=20identifier=20to=20= return=20during=20token=0A+=20*=09=09=09=09=09=09=09=09validation.=20= Defaults=20to=20the=20username=20in=20the=0A+=20*=09=09=09=09=09=09=09=09= startup=20packet.=0A+=20*=0A+=20*=20-=20oauth_validator.authorize_tokens=0A= +=20*=09=09=09=09=09=09=09=09Sets=20whether=20to=20successfully=20= validate=20incoming=0A+=20*=09=09=09=09=09=09=09=09tokens.=20Defaults=20= to=20true.=0A+=20*/=0A+void=0A+_PG_init(void)=0A+{=0A+=09= DefineCustomStringVariable("oauth_validator.authn_id",=0A+=09=09=09=09=09= =09=09=20=20=20"Authenticated=20identity=20to=20use=20for=20future=20= connections",=0A+=09=09=09=09=09=09=09=20=20=20NULL,=0A+=09=09=09=09=09=09= =09=20=20=20&authn_id,=0A+=09=09=09=09=09=09=09=20=20=20NULL,=0A+=09=09=09= =09=09=09=09=20=20=20PGC_SIGHUP,=0A+=09=09=09=09=09=09=09=20=20=200,=0A+=09= =09=09=09=09=09=09=20=20=20NULL,=20NULL,=20NULL);=0A+=09= DefineCustomBoolVariable("oauth_validator.authorize_tokens",=0A+=09=09=09= =09=09=09=09=20"Should=20tokens=20be=20marked=20valid?",=0A+=09=09=09=09=09= =09=09=20NULL,=0A+=09=09=09=09=09=09=09=20&authorize_tokens,=0A+=09=09=09= =09=09=09=09=20true,=0A+=09=09=09=09=09=09=09=20PGC_SIGHUP,=0A+=09=09=09=09= =09=09=09=200,=0A+=09=09=09=09=09=09=09=20NULL,=20NULL,=20NULL);=0A+=0A+=09= MarkGUCPrefixReserved("oauth_validator");=0A+}=0A+=0A+/*=0A+=20*=20= Validator=20module=20entry=20point.=0A+=20*/=0A+const=20= OAuthValidatorCallbacks=20*=0A+_PG_oauth_validator_module_init(void)=0A= +{=0A+=09return=20&validator_callbacks;=0A+}=0A+=0A+#define=20= PRIVATE_COOKIE=20((void=20*)=2013579)=0A+=0A+/*=0A+=20*=20Startup=20= callback,=20to=20set=20up=20private=20data=20for=20the=20validator.=0A+=20= */=0A+static=20void=0A+validator_startup(ValidatorModuleState=20*state)=0A= +{=0A+=09state->private_data=20=3D=20PRIVATE_COOKIE;=0A+}=0A+=0A+/*=0A+=20= *=20Shutdown=20callback,=20to=20tear=20down=20the=20validator.=0A+=20*/=0A= +static=20void=0A+validator_shutdown(ValidatorModuleState=20*state)=0A+{=0A= +=09/*=20Check=20to=20make=20sure=20our=20private=20state=20still=20= exists.=20*/=0A+=09if=20(state->private_data=20!=3D=20PRIVATE_COOKIE)=0A= +=09=09elog(PANIC,=20"oauth_validator:=20private=20state=20cookie=20= changed=20to=20%p=20in=20shutdown",=0A+=09=09=09=20state->private_data);=0A= +}=0A+=0A+/*=0A+=20*=20Validator=20implementation.=20Logs=20the=20= incoming=20data=20and=20authorizes=20the=20token=20by=0A+=20*=20default;=20= the=20behavior=20can=20be=20modified=20via=20the=20module's=20GUC=20= settings.=0A+=20*/=0A+static=20ValidatorModuleResult=20*=0A= +validate_token(ValidatorModuleState=20*state,=20const=20char=20*token,=20= const=20char=20*role)=0A+{=0A+=09ValidatorModuleResult=20*res;=0A+=0A+=09= /*=20Check=20to=20make=20sure=20our=20private=20state=20still=20exists.=20= */=0A+=09if=20(state->private_data=20!=3D=20PRIVATE_COOKIE)=0A+=09=09= elog(ERROR,=20"oauth_validator:=20private=20state=20cookie=20changed=20= to=20%p=20in=20validate",=0A+=09=09=09=20state->private_data);=0A+=0A+=09= res=20=3D=20palloc(sizeof(ValidatorModuleResult));=0A+=0A+=09elog(LOG,=20= "oauth_validator:=20token=3D\"%s\",=20role=3D\"%s\"",=20token,=20role);=0A= +=09elog(LOG,=20"oauth_validator:=20issuer=3D\"%s\",=20scope=3D\"%s\"",=0A= +=09=09=20MyProcPort->hba->oauth_issuer,=0A+=09=09=20= MyProcPort->hba->oauth_scope);=0A+=0A+=09res->authorized=20=3D=20= authorize_tokens;=0A+=09if=20(authn_id)=0A+=09=09res->authn_id=20=3D=20= pstrdup(authn_id);=0A+=09else=0A+=09=09res->authn_id=20=3D=20= pstrdup(role);=0A+=0A+=09return=20res;=0A+}=0Adiff=20--git=20= a/src/test/perl/PostgreSQL/Test/Cluster.pm=20= b/src/test/perl/PostgreSQL/Test/Cluster.pm=0Aindex=20= f521ad0b12f..ab7d7452ede=20100644=0A---=20= a/src/test/perl/PostgreSQL/Test/Cluster.pm=0A+++=20= b/src/test/perl/PostgreSQL/Test/Cluster.pm=0A@@=20-2515,6=20+2515,11=20= @@=20instead=20of=20the=20default.=0A=20=0A=20If=20this=20regular=20= expression=20is=20set,=20matches=20it=20with=20the=20output=20generated.=0A= =20=0A+=3Ditem=20expected_stderr=20=3D>=20B=0A+=0A+If=20this=20= regular=20expression=20is=20set,=20matches=20it=20against=20the=20= standard=20error=0A+stream;=20otherwise=20the=20stderr=20must=20be=20= empty.=0A+=0A=20=3Ditem=20log_like=20=3D>=20[=20qr/required=20message/=20= ]=0A=20=0A=20=3Ditem=20log_unlike=20=3D>=20[=20qr/prohibited=20message/=20= ]=0A@@=20-2558,7=20+2563,20=20@@=20sub=20connect_ok=0A=20=09=09= like($stdout,=20$params{expected_stdout},=20"$test_name:=20stdout=20= matches");=0A=20=09}=0A=20=0A-=09is($stderr,=20"",=20"$test_name:=20no=20= stderr");=0A+=09if=20(defined($params{expected_stderr}))=0A+=09{=0A+=09=09= if=20(like($stderr,=20$params{expected_stderr},=20"$test_name:=20stderr=20= matches")=0A+=09=09=09&&=20($ret=20!=3D=200))=0A+=09=09{=0A+=09=09=09#=20= In=20this=20case=20(failing=20test=20but=20matching=20stderr)=20we'll=20= have=0A+=09=09=09#=20swallowed=20the=20output=20needed=20to=20debug.=20= Put=20it=20back=20into=20the=20logs.=0A+=09=09=09diag("$test_name:=20= full=20stderr:\n"=20.=20$stderr);=0A+=09=09}=0A+=09}=0A+=09else=0A+=09{=0A= +=09=09is($stderr,=20"",=20"$test_name:=20no=20stderr");=0A+=09}=0A=20=0A= =20=09$self->log_check($test_name,=20$log_location,=20%params);=0A=20}=0A= diff=20--git=20a/src/tools/pgindent/pgindent=20= b/src/tools/pgindent/pgindent=0Aindex=20d8acce7e929..7dccf4614aa=20= 100755=0A---=20a/src/tools/pgindent/pgindent=0A+++=20= b/src/tools/pgindent/pgindent=0A@@=20-242,6=20+242,14=20@@=20sub=20= pre_indent=0A=20=09#=20Protect=20wrapping=20in=20CATALOG()=0A=20=09= $source=20=3D~=20s!^(CATALOG\(.*)$!/*$1*/!gm;=0A=20=0A+=09#=20Treat=20a=20= CURL_IGNORE_DEPRECATION()=20as=20braces=20for=20the=20purposes=20of=0A+=09= #=20indentation.=20(The=20recursive=20regex=20comes=20from=20the=20= perlre=20documentation;=20it=0A+=09#=20matches=20balanced=20parentheses=20= as=20group=20$1=20and=20the=20contents=20as=20group=20$2.)=0A+=09my=20= $curlopen=20=3D=20'{=20/*=20CURL_IGNORE_DEPRECATION=20*/';=0A+=09my=20= $curlclose=20=3D=20'}=20/*=20CURL_IGNORE_DEPRECATION=20*/';=0A+=09= $source=20=3D~=0A+=09=20=20s!^[=20= \t]+CURL_IGNORE_DEPRECATION(\(((?:(?>[^()]+)|(?1))*)\))!$curlopen$2$curlcl= ose!gms;=0A+=0A=20=09return=20$source;=0A=20}=0A=20=0A@@=20-256,6=20= +264,12=20@@=20sub=20post_indent=0A=20=09$source=20=3D~=20s!^/\*=20Open=20= extern=20"C"=20\*/$!{!gm;=0A=20=09$source=20=3D~=20s!^/\*=20Close=20= extern=20"C"=20\*/$!}!gm;=0A=20=0A+=09#=20Restore=20the=20= CURL_IGNORE_DEPRECATION()=20macro,=20keeping=20in=20mind=20that=20our=0A= +=09#=20markers=20may=20have=20been=20re-indented.=0A+=09$source=20=3D~=0A= +=09=20=20s!{[=20\t]+/\*=20CURL_IGNORE_DEPRECATION=20= \*/!CURL_IGNORE_DEPRECATION(!gm;=0A+=09$source=20=3D~=20s!}[=20\t]+/\*=20= CURL_IGNORE_DEPRECATION=20\*/!)!gm;=0A+=0A=20=09##=20Comments=0A=20=0A=20= =09#=20Undo=20change=20of=20dash-protected=20block=20comments=0Adiff=20= --git=20a/src/tools/pgindent/typedefs.list=20= b/src/tools/pgindent/typedefs.list=0Aindex=20b6c170ac249..ed8ef8ddc89=20= 100644=0A---=20a/src/tools/pgindent/typedefs.list=0A+++=20= b/src/tools/pgindent/typedefs.list=0A@@=20-371,6=20+371,9=20@@=20CState=0A= =20CTECycleClause=0A=20CTEMaterialize=0A=20CTESearchClause=0A+CURL=0A= +CURLM=0A+CURLoption=0A=20CV=0A=20CachedExpression=0A=20CachedPlan=0A@@=20= -1724,6=20+1727,7=20@@=20NumericDigit=0A=20NumericSortSupport=0A=20= NumericSumAccum=0A=20NumericVar=0A+OAuthValidatorCallbacks=0A=20= OM_uint32=0A=20OP=0A=20OSAPerGroupState=0A@@=20-1832,6=20+1836,7=20@@=20= PGVerbosity=0A=20PG_Locale_Strategy=0A=20PG_Lock_Status=0A=20PG_init_t=0A= +PGauthData=0A=20PGcancel=0A=20PGcancelConn=0A=20PGcmdQueueEntry=0A@@=20= -1839,7=20+1844,9=20@@=20PGconn=0A=20PGdataValue=0A=20PGlobjfuncs=0A=20= PGnotify=0A+PGoauthBearerRequest=0A=20PGpipelineStatus=0A= +PGpromptOAuthDevice=0A=20PGresAttDesc=0A=20PGresAttValue=0A=20= PGresParamDesc=0A@@=20-1952,6=20+1959,7=20@@=20PQArgBlock=0A=20= PQEnvironmentOption=0A=20PQExpBuffer=0A=20PQExpBufferData=0A= +PQauthDataHook_type=0A=20PQcommMethods=0A=20PQconninfoOption=0A=20= PQnoticeProcessor=0A@@=20-3091,6=20+3099,8=20@@=20VacuumRelation=0A=20= VacuumStmt=0A=20ValidIOData=0A=20ValidateIndexState=0A= +ValidatorModuleState=0A+ValidatorModuleResult=0A=20ValuesScan=0A=20= ValuesScanState=0A=20Var=0A@@=20-3488,6=20+3498,7=20@@=20= explain_get_index_name_hook_type=0A=20f_smgr=0A=20fasthash_state=0A=20= fd_set=0A+fe_oauth_state=0A=20fe_scram_state=0A=20fe_scram_state_enum=0A=20= fetch_range_request=0A--=20=0A2.39.3=20(Apple=20Git-146)=0A=0A= --Apple-Mail=_43C3B3B1-87EA-4831-9D4F-5C67D4DD918F Content-Disposition: attachment; filename=v49-0002-v48-fixup-patches-Add-OAUTHBEARER-SASL-mechanism.patch Content-Type: application/octet-stream; x-unix-mode=0644; name="v49-0002-v48-fixup-patches-Add-OAUTHBEARER-SASL-mechanism.patch" Content-Transfer-Encoding: quoted-printable =46rom=20e723c2040405237155eeca58cab675866763b876=20Mon=20Sep=2017=20= 00:00:00=202001=0AFrom:=20Jacob=20Champion=20= =0ADate:=20Fri,=207=20Feb=202025=20= 14:23:40=20-0800=0ASubject:=20[PATCH=20v49=202/4]=20v48=20fixup=20= patches!=20Add=20OAUTHBEARER=20SASL=20mechanism=0A=0A---=0A=20= doc/src/sgml/libpq.sgml=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20|=2040=20+++++++++++++++-=0A=20= doc/src/sgml/oauth-validators.sgml=20=20=20=20=20=20=20=20=20=20=20=20|=20= 31=20++++++++----=0A=20src/backend/libpq/auth-oauth.c=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20|=2020=20++++++--=0A=20= src/include/libpq/oauth.h=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20|=2048=20++++++++++++++++++-=0A=20= src/interfaces/libpq/fe-auth-oauth-curl.c=20=20=20=20=20|=2014=20+++++-=0A= =20.../modules/oauth_validator/fail_validator.c=20=20|=2015=20++++--=0A=20= .../modules/oauth_validator/t/001_server.pl=20=20=20|=2011=20++++-=0A=20= src/test/modules/oauth_validator/validator.c=20=20|=2028=20+++++++----=0A= =208=20files=20changed,=20175=20insertions(+),=2032=20deletions(-)=0A=0A= diff=20--git=20a/doc/src/sgml/libpq.sgml=20b/doc/src/sgml/libpq.sgml=0A= index=20a51355e238f..b2abae8deee=20100644=0A---=20= a/doc/src/sgml/libpq.sgml=0A+++=20b/doc/src/sgml/libpq.sgml=0A@@=20= -10133,8=20+10133,46=20@@=20void=20PQinitSSL(int=20do_ssl);=0A=20=20=20= OAuth=20Support=0A=20=0A=20=20=20=0A-=20=20=20TODO=0A= +=20=20=20libpq=20implements=20support=20for=20the=20OAuth=20v2=20Device=20= Authorization=20client=20flow,=0A+=20=20=20documented=20in=0A+=20=20=20= RFC=20= 8628,=0A+=20=20=20which=20it=20will=20attempt=20to=20use=20by=20= default=20if=20the=20server=0A+=20=20=20requests=20a=20bearer=20token=20during=0A+=20= =20=20authentication.=20This=20flow=20can=20be=20utilized=20even=20if=20= the=20system=20running=20the=0A+=20=20=20client=20application=20does=20= not=20have=20a=20usable=20web=20browser,=20for=20example=20when=0A+=20=20= =20running=20a=20client=20via=20SSH.=20Client=20applications=20may=20= implement=20their=20own=20flows=0A+=20=20=20instead;=20see=20.=0A=20=20=20=0A+=20=20= =0A+=20=20=20The=20builtin=20flow=20will,=20by=20default,=20print=20= a=20URL=20to=20visit=20and=20a=20user=20code=20to=0A+=20=20=20enter=20= there:=0A+=0A+$=20psql=20'dbname=3Dpostgres=20= oauth_issuer=3Dhttps://example.com=20oauth_client_id=3D...'=0A+Visit=20= https://example.com/device=20and=20enter=20the=20code:=20ABCD-EFGH=0A= +=0A+=20=20=20(This=20prompt=20may=20be=0A+=20=20=20= customized.)=0A= +=20=20=20You=20will=20then=20log=20into=20your=20OAuth=20provider,=20= which=20will=20ask=20whether=20you=20want=0A+=20=20=20to=20allow=20libpq=20= and=20the=20server=20to=20perform=20actions=20on=20your=20behalf.=20It=20= is=20always=0A+=20=20=20a=20good=20idea=20to=20carefully=20review=20the=20= URL=20and=20permissions=20displayed,=20to=20ensure=0A+=20=20=20they=20= match=20your=20expectations,=20before=20continuing.=20Do=20not=20give=20= permissions=20to=0A+=20=20=20untrusted=20third=20parties.=0A+=20=20= =0A+=20=20=0A+=20=20=20For=20an=20OAuth=20client=20flow=20= to=20be=20usable,=20the=20connection=20string=20must=20at=20minimum=0A+=20= =20=20contain=20=20and=0A= +=20=20=20.=20(These=20= settings=20are=0A+=20=20=20determined=20by=20your=20organization's=20= OAuth=20provider.)=20The=20builtin=20flow=0A+=20=20=20additionally=20= requires=20the=20OAuth=20authorization=20server=20to=20publish=20a=20= device=0A+=20=20=20authorization=20endpoint.=0A+=20=20=0A+=0A+=20=20= =0A+=20=20=20=0A+=20=20=20=20The=20builtin=20Device=20= Authorization=20flow=20is=20not=20currently=20supported=20on=20Windows.=0A= +=20=20=20=20Custom=20client=20flows=20may=20still=20be=20implemented.=0A= +=20=20=20=0A+=20=20=0A=20=0A=20=20=20=0A=20=20=20=20Authdata=20= Hooks=0Adiff=20--git=20a/doc/src/sgml/oauth-validators.sgml=20= b/doc/src/sgml/oauth-validators.sgml=0Aindex=20d0bca9196d9..eb8c4431c2d=20= 100644=0A---=20a/doc/src/sgml/oauth-validators.sgml=0A+++=20= b/doc/src/sgml/oauth-validators.sgml=0A@@=20-41,7=20+41,9=20@@=0A=20=20=20= =0A=20=20=20=20= Validator=20Responsibilities=0A=20=20=20=20=0A-=20=20= =20=20TODO=0A+=20=20=20=20Although=20different=20modules=20may=20take=20= very=20different=20approaches=20to=20token=0A+=20=20=20=20validation,=20= implementations=20generally=20need=20to=20perform=20three=20separate=0A+=20= =20=20=20actions:=0A=20=20=20=20=0A=20=20=20=20=0A=20= =20=20=20=20=0A@@=20-121,6=20+123,11=20@@=0A=20=20=20=20=20= =20=20=20=0A=20=20=20=20=20=20=20=20if=20users=20are=20not=20= prompted=20for=20additional=20scopes.=0A=20=20=20=20=20=20=20=0A+=20= =20=20=20=20=20=0A+=20=20=20=20=20=20=20Even=20if=20authorization=20= fails,=20a=20module=20may=20choose=20to=20continue=20to=20pull=0A+=20=20=20= =20=20=20=20authentication=20information=20from=20the=20token=20for=20= use=20in=20auditing=20and=0A+=20=20=20=20=20=20=20debugging.=0A+=20=20=20= =20=20=20=0A=20=20=20=20=20=20=0A=20=20=20=20=20= =0A=20=20=20=20=20=0A@@=20-290,13=20+297,15=20= @@=0A=20=20=20=20validator=20module=20a=20function=20named=0A=20=20=20=20= _PG_oauth_validator_module_init=20must=20be=20= provided.=20The=0A=20=20=20=20return=20value=20of=20the=20function=20= must=20be=20a=20pointer=20to=20a=20struct=20of=20type=0A-=20=20=20= OAuthValidatorCallbacks,=20which=20contains=20= pointers=20to=0A-=20=20=20the=20module's=20token=20validation=20= functions.=20The=20returned=0A+=20=20=20= OAuthValidatorCallbacks,=20which=20contains=20a=20= magic=0A+=20=20=20number=20and=20pointers=20to=20the=20module's=20token=20= validation=20functions.=20The=20returned=0A=20=20=20=20pointer=20must=20= be=20of=20server=20lifetime,=20which=20is=20typically=20achieved=20by=20= defining=0A=20=20=20=20it=20as=20a=20static=20const=20= variable=20in=20global=20scope.=0A=20=0A=20typedef=20= struct=20OAuthValidatorCallbacks=0A=20{=0A+=20=20=20=20uint32=20=20=20=20= =20=20=20=20magic;=20=20=20=20=20=20=20=20=20=20=20=20/*=20must=20be=20= set=20to=20PG_OAUTH_VALIDATOR_MAGIC=20*/=0A+=0A=20=20=20=20=20= ValidatorStartupCB=20startup_cb;=0A=20=20=20=20=20ValidatorShutdownCB=20= shutdown_cb;=0A=20=20=20=20=20ValidatorValidateCB=20validate_cb;=0A@@=20= -341,14=20+350,16=20@@=20typedef=20void=20(*ValidatorStartupCB)=20= (ValidatorModuleState=20*state);=0A=20=20=20=20=20previous=20calls=20= will=20be=20available=20in=20= state->private_data.=0A=20=0A=20= =0A-typedef=20ValidatorModuleResult=20= *(*ValidatorValidateCB)=20(ValidatorModuleState=20*state,=20const=20char=20= *token,=20const=20char=20*role);=0A+typedef=20bool=20= (*ValidatorValidateCB)=20(const=20ValidatorModuleState=20*state,=0A+=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20const=20char=20*token,=20const=20char=20= *role,=0A+=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20ValidatorModuleResult=20= *result);=0A=20=0A=20=0A=20=20=20=20=20= token=20will=20contain=20the=20bearer=20token=20= to=20validate.=0A=20=20=20=20=20The=20server=20has=20ensured=20that=20= the=20token=20is=20well-formed=20syntactically,=20but=20no=0A=20=20=20=20= =20other=20validation=20has=20been=20performed.=20=20= role=20will=0A=20=20=20=20=20contain=20the=20= role=20the=20user=20has=20requested=20to=20log=20in=20as.=20=20The=20= callback=20must=0A-=20=20=20=20return=20a=20palloc'd=20= ValidatorModuleResult=20struct,=20which=20is=0A+=20=20= =20=20set=20output=20parameters=20in=20the=20result=20= struct,=20which=20is=0A=20=20=20=20=20defined=20as=20below:=0A=20=0A=20= =0A@@=20-368,17=20+379,17=20@@=20typedef=20struct=20= ValidatorModuleResult=0A=20=20=20=20=20determined.=0A=20=20=20=20=0A= =20=20=20=20=0A-=20=20=20=20The=20caller=20assumes=20ownership=20= of=20the=20returned=20memory=20allocation,=20the=0A-=20=20=20=20= validator=20module=20should=20not=20in=20any=20way=20access=20the=20= memory=20after=20it=20has=20been=0A-=20=20=20=20returned.=20=20A=20= validator=20may=20instead=20return=20NULL=20to=20signal=20an=20internal=0A= -=20=20=20=20error.=0A+=20=20=20=20A=20validator=20may=20return=20= false=20to=20signal=20an=20internal=20error,=0A+=20=20= =20=20in=20which=20case=20any=20result=20parameters=20are=20ignored=20= and=20the=20connection=20fails.=0A+=20=20=20=20Otherwise=20the=20= validator=20should=20return=20true=20to=20indicate=0A= +=20=20=20=20that=20it=20has=20processed=20the=20token=20and=20made=20an=20= authorization=20decision.=0A=20=20=20=20=0A=20=20=20=20=0A=20= =20=20=20=20The=20behavior=20after=20validate_cb=20= returns=20depends=20on=20the=0A=20=20=20=20=20specific=20HBA=20setup.=20=20= Normally,=20the=20authn_id=20user=0A=20=20=20=20= =20name=20must=20exactly=20match=20the=20role=20that=20the=20user=20is=20= logging=20in=20as.=20=20(This=0A=20=20=20=20=20behavior=20may=20be=20= modified=20with=20a=20usermap.)=20=20But=20when=20authenticating=20= against=0A-=20=20=20=20an=20HBA=20rule=20with=20= trust_validator_authz=20turned=20on,=20the=0A+=20=20=20= =20an=20HBA=20rule=20with=20delegate_ident_mapping=20= turned=20on,=20the=0A=20=20=20=20=20server=20will=20not=20perform=20any=20= checks=20on=20the=20value=20of=0A=20=20=20=20=20= authn_id=20at=20all;=20in=20this=20case=20it=20= is=20up=20to=20the=0A=20=20=20=20=20validator=20to=20ensure=20that=20the=20= token=20carries=20enough=20privileges=20for=20the=20user=20to=0Adiff=20= --git=20a/src/backend/libpq/auth-oauth.c=20= b/src/backend/libpq/auth-oauth.c=0Aindex=20aa16977c643..e2b5d1ed913=20= 100644=0A---=20a/src/backend/libpq/auth-oauth.c=0A+++=20= b/src/backend/libpq/auth-oauth.c=0A@@=20-656,9=20+656,9=20@@=20= validate(Port=20*port,=20const=20char=20*auth)=0A=20=09=09=09=09= errmsg("validation=20of=20OAuth=20token=20requested=20without=20a=20= validator=20loaded"));=0A=20=0A=20=09/*=20Call=20the=20validation=20= function=20from=20the=20validator=20module=20*/=0A-=09ret=20=3D=20= ValidatorCallbacks->validate_cb(validator_module_state,=0A-=09=09=09=09=09= =09=09=09=09=09=20=20token,=20port->user_name);=0A-=09if=20(ret=20=3D=3D=20= NULL)=0A+=09ret=20=3D=20palloc0(sizeof(ValidatorModuleResult));=0A+=09if=20= (!ValidatorCallbacks->validate_cb(validator_module_state,=20token,=0A+=09= =09=09=09=09=09=09=09=09=09=20port->user_name,=20ret))=0A=20=09{=0A=20=09= =09ereport(LOG,=20errmsg("internal=20error=20in=20OAuth=20validator=20= module"));=0A=20=09=09return=20false;=0A@@=20-756,8=20+756,22=20@@=20= load_validator_library(const=20char=20*libname)=0A=20=09= ValidatorCallbacks=20=3D=20(*validator_init)=20();=0A=20=09= Assert(ValidatorCallbacks);=0A=20=0A+=09/*=0A+=09=20*=20Check=20the=20= magic=20number,=20to=20protect=20against=20break-glass=20scenarios=20= where=0A+=09=20*=20the=20ABI=20must=20change=20within=20a=20major=20= version.=20load_external_function()=0A+=09=20*=20already=20checks=20for=20= compatibility=20across=20major=20versions.=0A+=09=20*/=0A+=09if=20= (ValidatorCallbacks->magic=20!=3D=20PG_OAUTH_VALIDATOR_MAGIC)=0A+=09=09= ereport(ERROR,=0A+=09=09=09=09errmsg("%s=20module=20\"%s\":=20magic=20= number=20mismatch",=0A+=09=09=09=09=09=20=20=20"OAuth=20validator",=20= libname),=0A+=09=09=09=09errdetail("Server=20has=20magic=20number=20= 0x%08X,=20module=20has=200x%08X.",=0A+=09=09=09=09=09=09=20=20= PG_OAUTH_VALIDATOR_MAGIC,=20ValidatorCallbacks->magic));=0A+=0A=20=09/*=20= Allocate=20memory=20for=20validator=20library=20private=20state=20data=20= */=0A=20=09validator_module_state=20=3D=20(ValidatorModuleState=20*)=20= palloc0(sizeof(ValidatorModuleState));=0A+=09= validator_module_state->sversion=20=3D=20PG_VERSION_NUM;=0A+=0A=20=09if=20= (ValidatorCallbacks->startup_cb=20!=3D=20NULL)=0A=20=09=09= ValidatorCallbacks->startup_cb(validator_module_state);=0A=20=0Adiff=20= --git=20a/src/include/libpq/oauth.h=20b/src/include/libpq/oauth.h=0A= index=204fcdda74305..7e249613e10=20100644=0A---=20= a/src/include/libpq/oauth.h=0A+++=20b/src/include/libpq/oauth.h=0A@@=20= -20,26=20+20,72=20@@=20extern=20PGDLLIMPORT=20char=20= *oauth_validator_libraries_string;=0A=20=0A=20typedef=20struct=20= ValidatorModuleState=0A=20{=0A+=09/*=20Holds=20the=20server's=20= PG_VERSION_NUM.=20Reserved=20for=20future=20extensibility.=20*/=0A+=09= int=09=09=09sversion;=0A+=0A+=09/*=0A+=09=20*=20Private=20data=20pointer=20= for=20use=20by=20a=20validator=20module.=20This=20can=20be=20used=20to=0A= +=09=20*=20store=20state=20for=20the=20module=20that=20will=20be=20= passed=20to=20each=20of=20its=0A+=09=20*=20callbacks.=0A+=09=20*/=0A=20=09= void=09=20=20=20*private_data;=0A=20}=20ValidatorModuleState;=0A=20=0A=20= typedef=20struct=20ValidatorModuleResult=0A=20{=0A+=09/*=0A+=09=20*=20= Should=20be=20set=20to=20true=20if=20the=20token=20carries=20sufficient=20= permissions=20for=0A+=09=20*=20the=20bearer=20to=20connect.=0A+=09=20*/=0A= =20=09bool=09=09authorized;=0A+=0A+=09/*=0A+=09=20*=20If=20the=20token=20= authenticates=20the=20user,=20this=20should=20be=20set=20to=20a=20= palloc'd=0A+=09=20*=20string=20containing=20the=20SYSTEM_USER=20to=20use=20= for=20HBA=20mapping.=20Consider=0A+=09=20*=20setting=20this=20even=20if=20= result->authorized=20is=20false=20so=20that=20DBAs=20may=20use=0A+=09=20= *=20the=20logs=20to=20match=20end=20users=20to=20token=20failures.=0A+=09= =20*=0A+=09=20*=20This=20is=20required=20if=20the=20module=20is=20not=20= configured=20for=20ident=20mapping=0A+=09=20*=20delegation.=20See=20the=20= validator=20module=20documentation=20for=20details.=0A+=09=20*/=0A=20=09= char=09=20=20=20*authn_id;=0A=20}=20ValidatorModuleResult;=0A=20=0A+/*=0A= +=20*=20Validator=20module=20callbacks=0A+=20*=0A+=20*=20These=20= callback=20functions=20should=20be=20defined=20by=20validator=20modules=20= and=20returned=0A+=20*=20via=20_PG_oauth_validator_module_init().=20=20= ValidatorValidateCB=20is=20the=20only=0A+=20*=20required=20callback.=20= For=20more=20information=20about=20the=20purpose=20of=20each=20callback,=0A= +=20*=20refer=20to=20the=20OAuth=20validator=20modules=20documentation.=0A= +=20*/=0A=20typedef=20void=20(*ValidatorStartupCB)=20= (ValidatorModuleState=20*state);=0A=20typedef=20void=20= (*ValidatorShutdownCB)=20(ValidatorModuleState=20*state);=0A-typedef=20= ValidatorModuleResult=20*(*ValidatorValidateCB)=20(ValidatorModuleState=20= *state,=20const=20char=20*token,=20const=20char=20*role);=0A+typedef=20= bool=20(*ValidatorValidateCB)=20(const=20ValidatorModuleState=20*state,=0A= +=09=09=09=09=09=09=09=09=09=20const=20char=20*token,=20const=20char=20= *role,=0A+=09=09=09=09=09=09=09=09=09=20ValidatorModuleResult=20= *result);=0A+=0A+/*=0A+=20*=20Identifies=20the=20compiled=20ABI=20= version=20of=20the=20validator=20module.=20Since=20the=20server=0A+=20*=20= already=20enforces=20the=20PG_MODULE_MAGIC=20number=20for=20modules=20= across=20major=0A+=20*=20versions,=20this=20is=20reserved=20for=20= emergency=20use=20within=20a=20stable=20release=20line.=0A+=20*=20May=20= it=20never=20need=20to=20change.=0A+=20*/=0A+#define=20= PG_OAUTH_VALIDATOR_MAGIC=200x20250207=0A=20=0A=20typedef=20struct=20= OAuthValidatorCallbacks=0A=20{=0A+=09uint32=09=09magic;=09=09=09/*=20= must=20be=20set=20to=20PG_OAUTH_VALIDATOR_MAGIC=20*/=0A+=0A=20=09= ValidatorStartupCB=20startup_cb;=0A=20=09ValidatorShutdownCB=20= shutdown_cb;=0A=20=09ValidatorValidateCB=20validate_cb;=0A=20}=20= OAuthValidatorCallbacks;=0A=20=0A+/*=0A+=20*=20Type=20of=20the=20shared=20= library=20symbol=20_PG_oauth_validator_module_init=20that=20is=0A+=20*=20= looked=20up=20when=20loading=20a=20validator=20module.=0A+=20*/=0A=20= typedef=20const=20OAuthValidatorCallbacks=20*(*OAuthValidatorModuleInit)=20= (void);=0A=20extern=20PGDLLEXPORT=20const=20OAuthValidatorCallbacks=20= *_PG_oauth_validator_module_init(void);=0A=20=0Adiff=20--git=20= a/src/interfaces/libpq/fe-auth-oauth-curl.c=20= b/src/interfaces/libpq/fe-auth-oauth-curl.c=0Aindex=20= 2179bb89800..74323de309a=20100644=0A---=20= a/src/interfaces/libpq/fe-auth-oauth-curl.c=0A+++=20= b/src/interfaces/libpq/fe-auth-oauth-curl.c=0A@@=20-32,7=20+32,19=20@@=0A= =20#include=20"libpq-int.h"=0A=20#include=20"mb/pg_wchar.h"=0A=20=0A= -#define=20MAX_OAUTH_RESPONSE_SIZE=20(1024=20*=201024)=0A+/*=0A+=20*=20= It's=20generally=20prudent=20to=20set=20a=20maximum=20response=20size=20= to=20buffer=20in=20memory,=0A+=20*=20but=20it's=20less=20clear=20what=20= size=20to=20choose.=20The=20biggest=20of=20our=20expected=0A+=20*=20= responses=20is=20the=20server=20metadata=20JSON,=20which=20will=20only=20= continue=20to=20grow=20in=0A+=20*=20size;=20the=20number=20of=20= IANA-registered=20parameters=20in=20that=20document=20is=20up=20to=2078=0A= +=20*=20as=20of=20February=202025.=0A+=20*=0A+=20*=20Even=20if=20every=20= single=20parameter=20were=20to=20take=20up=202k=20on=20average=20(a=20= previously=0A+=20*=20common=20limit=20on=20the=20size=20of=20a=20URL),=20= 256k=20gives=20us=20128=20parameter=20values=20before=0A+=20*=20we=20= give=20up.=20(That's=20almost=20certainly=20complete=20overkill=20in=20= practice;=202-4k=0A+=20*=20appears=20to=20be=20common=20among=20popular=20= providers=20at=20the=20moment.)=0A+=20*/=0A+#define=20= MAX_OAUTH_RESPONSE_SIZE=20(256=20*=201024)=0A=20=0A=20/*=0A=20=20*=20= Parsed=20JSON=20Representations=0Adiff=20--git=20= a/src/test/modules/oauth_validator/fail_validator.c=20= b/src/test/modules/oauth_validator/fail_validator.c=0Aindex=20= f77a3e115c6..7b1e69518d9=20100644=0A---=20= a/src/test/modules/oauth_validator/fail_validator.c=0A+++=20= b/src/test/modules/oauth_validator/fail_validator.c=0A@@=20-19,12=20= +19,15=20@@=0A=20=0A=20PG_MODULE_MAGIC;=0A=20=0A-static=20= ValidatorModuleResult=20*fail_token(ValidatorModuleState=20*state,=0A-=09= =09=09=09=09=09=09=09=09=09=20const=20char=20*token,=0A-=09=09=09=09=09=09= =09=09=09=09=20const=20char=20*role);=0A+static=20bool=20= fail_token(const=20ValidatorModuleState=20*state,=0A+=09=09=09=09=09=20=20= =20const=20char=20*token,=0A+=09=09=09=09=09=20=20=20const=20char=20= *role,=0A+=09=09=09=09=09=20=20=20ValidatorModuleResult=20*result);=0A=20= =0A=20/*=20Callback=20implementations=20(we=20only=20need=20the=20main=20= one)=20*/=0A=20static=20const=20OAuthValidatorCallbacks=20= validator_callbacks=20=3D=20{=0A+=09PG_OAUTH_VALIDATOR_MAGIC,=0A+=0A=20=09= .validate_cb=20=3D=20fail_token,=0A=20};=0A=20=0A@@=20-34,8=20+37,10=20= @@=20_PG_oauth_validator_module_init(void)=0A=20=09return=20= &validator_callbacks;=0A=20}=0A=20=0A-static=20ValidatorModuleResult=20*=0A= -fail_token(ValidatorModuleState=20*state,=20const=20char=20*token,=20= const=20char=20*role)=0A+static=20bool=0A+fail_token(const=20= ValidatorModuleState=20*state,=0A+=09=09=20=20=20const=20char=20*token,=20= const=20char=20*role,=0A+=09=09=20=20=20ValidatorModuleResult=20*res)=0A=20= {=0A=20=09elog(FATAL,=20"fail_validator:=20sentinel=20error");=0A=20=09= pg_unreachable();=0Adiff=20--git=20= a/src/test/modules/oauth_validator/t/001_server.pl=20= b/src/test/modules/oauth_validator/t/001_server.pl=0Aindex=20= f0b918390fd..d2dda62a2d4=20100644=0A---=20= a/src/test/modules/oauth_validator/t/001_server.pl=0A+++=20= b/src/test/modules/oauth_validator/t/001_server.pl=0A@@=20-14,12=20= +14,18=20@@=20use=20MIME::Base64=20qw(encode_base64);=0A=20use=20= PostgreSQL::Test::Cluster;=0A=20use=20PostgreSQL::Test::Utils;=0A=20use=20= Test::More;=0A+use=20Config;=0A=20=0A=20use=20FindBin;=0A=20use=20lib=20= $FindBin::RealBin;=0A=20=0A=20use=20OAuth::Server;=0A=20=0A+if=20= ($Config{osname}=20eq=20'MSWin32')=0A+{=0A+=09plan=20skip_all=20=3D>=20= 'OAuth=20server-side=20tests=20are=20not=20supported=20on=20Windows';=0A= +}=0A+=0A=20if=20(!$ENV{PG_TEST_EXTRA}=20||=20$ENV{PG_TEST_EXTRA}=20!~=20= /\boauth\b/)=0A=20{=0A=20=09plan=20skip_all=20=3D>=0A@@=20-402,7=20= +408,10=20@@=20note=20"running=20'"=20.=20join("'=20'",=20@cmd)=20.=20= "'";=0A=20my=20($stdout,=20$stderr)=20=3D=20run_command(\@cmd);=0A=20=0A=20= like($stdout,=20qr/connection=20succeeded/,=20"stress-async:=20stdout=20= matches");=0A-unlike($stderr,=20qr/connection=20to=20database=20failed/,=20= "stress-async:=20stderr=20matches");=0A+unlike(=0A+=09$stderr,=0A+=09= qr/connection=20to=20database=20failed/,=0A+=09"stress-async:=20stderr=20= matches");=0A=20=0A=20#=0A=20#=20This=20section=20of=20tests=20= reconfigures=20the=20validator=20module=20itself,=20rather=20than=0Adiff=20= --git=20a/src/test/modules/oauth_validator/validator.c=20= b/src/test/modules/oauth_validator/validator.c=0Aindex=20= ef9bbb2866f..e218f5c8902=20100644=0A---=20= a/src/test/modules/oauth_validator/validator.c=0A+++=20= b/src/test/modules/oauth_validator/validator.c=0A@@=20-23,12=20+23,15=20= @@=20PG_MODULE_MAGIC;=0A=20=0A=20static=20void=20= validator_startup(ValidatorModuleState=20*state);=0A=20static=20void=20= validator_shutdown(ValidatorModuleState=20*state);=0A-static=20= ValidatorModuleResult=20*validate_token(ValidatorModuleState=20*state,=0A= -=09=09=09=09=09=09=09=09=09=09=09=20const=20char=20*token,=0A-=09=09=09=09= =09=09=09=09=09=09=09=20const=20char=20*role);=0A+static=20bool=20= validate_token(const=20ValidatorModuleState=20*state,=0A+=09=09=09=09=09=09= =20=20=20const=20char=20*token,=0A+=09=09=09=09=09=09=20=20=20const=20= char=20*role,=0A+=09=09=09=09=09=09=20=20=20ValidatorModuleResult=20= *result);=0A=20=0A=20/*=20Callback=20implementations=20(exercise=20all=20= three)=20*/=0A=20static=20const=20OAuthValidatorCallbacks=20= validator_callbacks=20=3D=20{=0A+=09PG_OAUTH_VALIDATOR_MAGIC,=0A+=0A=20=09= .startup_cb=20=3D=20validator_startup,=0A=20=09.shutdown_cb=20=3D=20= validator_shutdown,=0A=20=09.validate_cb=20=3D=20validate_token=0A@@=20= -89,6=20+92,13=20@@=20_PG_oauth_validator_module_init(void)=0A=20static=20= void=0A=20validator_startup(ValidatorModuleState=20*state)=0A=20{=0A+=09= /*=0A+=09=20*=20Make=20sure=20the=20server=20is=20correctly=20setting=20= sversion.=20(Real=20modules=0A+=09=20*=20should=20not=20do=20this;=20it=20= would=20defeat=20upgrade=20compatibility.)=0A+=09=20*/=0A+=09if=20= (state->sversion=20!=3D=20PG_VERSION_NUM)=0A+=09=09elog(ERROR,=20= "oauth_validator:=20sversion=20set=20to=20%d",=20state->sversion);=0A+=0A= =20=09state->private_data=20=3D=20PRIVATE_COOKIE;=0A=20}=0A=20=0A@@=20= -108,18=20+118,16=20@@=20validator_shutdown(ValidatorModuleState=20= *state)=0A=20=20*=20Validator=20implementation.=20Logs=20the=20incoming=20= data=20and=20authorizes=20the=20token=20by=0A=20=20*=20default;=20the=20= behavior=20can=20be=20modified=20via=20the=20module's=20GUC=20settings.=0A= =20=20*/=0A-static=20ValidatorModuleResult=20*=0A= -validate_token(ValidatorModuleState=20*state,=20const=20char=20*token,=20= const=20char=20*role)=0A+static=20bool=0A+validate_token(const=20= ValidatorModuleState=20*state,=0A+=09=09=09=20=20=20const=20char=20= *token,=20const=20char=20*role,=0A+=09=09=09=20=20=20= ValidatorModuleResult=20*res)=0A=20{=0A-=09ValidatorModuleResult=20*res;=0A= -=0A=20=09/*=20Check=20to=20make=20sure=20our=20private=20state=20still=20= exists.=20*/=0A=20=09if=20(state->private_data=20!=3D=20PRIVATE_COOKIE)=0A= =20=09=09elog(ERROR,=20"oauth_validator:=20private=20state=20cookie=20= changed=20to=20%p=20in=20validate",=0A=20=09=09=09=20= state->private_data);=0A=20=0A-=09res=20=3D=20= palloc(sizeof(ValidatorModuleResult));=0A-=0A=20=09elog(LOG,=20= "oauth_validator:=20token=3D\"%s\",=20role=3D\"%s\"",=20token,=20role);=0A= =20=09elog(LOG,=20"oauth_validator:=20issuer=3D\"%s\",=20scope=3D\"%s\"",=0A= =20=09=09=20MyProcPort->hba->oauth_issuer,=0A@@=20-131,5=20+139,5=20@@=20= validate_token(ValidatorModuleState=20*state,=20const=20char=20*token,=20= const=20char=20*role)=0A=20=09else=0A=20=09=09res->authn_id=20=3D=20= pstrdup(role);=0A=20=0A-=09return=20res;=0A+=09return=20true;=0A=20}=0A= --=20=0A2.39.3=20(Apple=20Git-146)=0A=0A= --Apple-Mail=_43C3B3B1-87EA-4831-9D4F-5C67D4DD918F Content-Disposition: attachment; filename=v49-0003-XXX-fix-libcurl-link-error.patch Content-Type: application/octet-stream; x-unix-mode=0644; name="v49-0003-XXX-fix-libcurl-link-error.patch" Content-Transfer-Encoding: quoted-printable =46rom=2003742dd74dd3948c2d03b13009b08fbe09f928d8=20Mon=20Sep=2017=20= 00:00:00=202001=0AFrom:=20Jacob=20Champion=20= =0ADate:=20Mon,=2013=20Jan=202025=20= 12:31:59=20-0800=0ASubject:=20[PATCH=20v49=203/4]=20XXX=20fix=20libcurl=20= link=20error=0A=0AThe=20ftp/curl=20port=20appears=20to=20be=20missing=20= a=20minimum=20version=20dependency=20on=0Alibssh2,=20so=20the=20= following=20starts=20showing=20up=20after=20upgrading=20to=20curl=0A= 8.11.1_1:=0A=0A=20=20=20=20libcurl.so.4:=20Undefined=20symbol=20= "libssh2_session_callback_set2"=0A=0ABut=2013.3=20is=20EOL,=20so=20it's=20= not=20clear=20if=20anyone=20would=20be=20interested=20in=20a=0Abug=20= report,=20and=20a=20FreeBSD=2014=20Cirrus=20image=20is=20in=20progress.=20= Hack=20past=20it=0Afor=20now.=0A---=0A=20.cirrus.tasks.yml=20|=201=20+=0A= =201=20file=20changed,=201=20insertion(+)=0A=0Adiff=20--git=20= a/.cirrus.tasks.yml=20b/.cirrus.tasks.yml=0Aindex=20= 2f5f5ef21a8..91b51142d2e=20100644=0A---=20a/.cirrus.tasks.yml=0A+++=20= b/.cirrus.tasks.yml=0A@@=20-168,6=20+168,7=20@@=20task:=0A=20=20=20=20=20= sysctl=20kern.corefile=3D'/tmp/cores/%N.%P.core'=0A=20=20=20= setup_additional_packages_script:=20|=0A=20=20=20=20=20pkg=20install=20= -y=20curl=0A+=20=20=20=20pkg=20upgrade=20-y=20libssh2=20#=20XXX=20= shouldn't=20be=20necessary.=20revisit=20w/=20FreeBSD=2014=0A=20=0A=20=20=20= #=20NB:=20Intentionally=20build=20without=20-Dllvm.=20The=20freebsd=20= image=20size=20is=20already=0A=20=20=20#=20large=20enough=20to=20make=20= VM=20startup=20slow,=20and=20even=20without=20llvm=20freebsd=0A--=20=0A= 2.39.3=20(Apple=20Git-146)=0A=0A= --Apple-Mail=_43C3B3B1-87EA-4831-9D4F-5C67D4DD918F Content-Disposition: attachment; filename=v49-0004-v49-Fixups-proposed-by-Daniel.patch Content-Type: application/octet-stream; x-unix-mode=0644; name="v49-0004-v49-Fixups-proposed-by-Daniel.patch" Content-Transfer-Encoding: quoted-printable =46rom=20eaa6078e9fe70940c359cca0883227746456fc94=20Mon=20Sep=2017=20= 00:00:00=202001=0AFrom:=20Daniel=20Gustafsson=20=0A= Date:=20Thu,=2013=20Feb=202025=2023:45:50=20+0100=0ASubject:=20[PATCH=20= v49=204/4]=20v49=20Fixups=20proposed=20by=20Daniel=0A=0A---=0A=20= config/programs.m4=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20|=20=204=20+-=0A=20= doc/src/sgml/client-auth.sgml=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20|=20=208=20+-=0A=20doc/src/sgml/installation.sgml=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20|=2010=20+--=0A=20doc/src/sgml/libpq.sgml=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20|=2025=20= +++---=0A=20doc/src/sgml/oauth-validators.sgml=20=20=20=20=20=20=20=20=20= =20=20=20|=2027=20++++---=0A=20meson.build=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= |=20=208=20+-=0A=20src/backend/libpq/auth-oauth.c=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20|=2079=20+++++++++++++------=0A=20= src/include/common/oauth-common.h=20=20=20=20=20=20=20=20=20=20=20=20=20= |=20=202=20+-=0A=20src/include/libpq/oauth.h=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20|=20=207=20+-=0A=20= src/include/pg_config.h.in=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20|=20=202=20+-=0A=20src/interfaces/libpq/fe-auth-oauth-curl.c=20= =20=20=20=20|=2018=20+++--=0A=20src/interfaces/libpq/fe-auth-oauth.c=20=20= =20=20=20=20=20=20=20=20|=20=204=20+-=0A=20= src/test/modules/oauth_validator/Makefile=20=20=20=20=20|=20=202=20+-=0A=20= src/test/modules/oauth_validator/README=20=20=20=20=20=20=20|=20=206=20= +-=0A=20.../modules/oauth_validator/fail_validator.c=20=20|=20=206=20+-=0A= =20.../modules/oauth_validator/magic_validator.c=20|=2048=20+++++++++++=0A= =20src/test/modules/oauth_validator/meson.build=20=20|=2018=20++++-=0A=20= .../oauth_validator/oauth_hook_client.c=20=20=20=20=20=20=20|=20=202=20= +-=0A=20.../modules/oauth_validator/t/001_server.pl=20=20=20|=2031=20= ++++++--=0A=20.../modules/oauth_validator/t/002_client.pl=20=20=20|=20=20= 2=20+-=0A=20.../modules/oauth_validator/t/OAuth/Server.pm=20|=20=204=20= +-=0A=20src/test/modules/oauth_validator/validator.c=20=20|=20=202=20+-=0A= =2022=20files=20changed,=20220=20insertions(+),=2095=20deletions(-)=0A=20= create=20mode=20100644=20= src/test/modules/oauth_validator/magic_validator.c=0A=0Adiff=20--git=20= a/config/programs.m4=20b/config/programs.m4=0Aindex=20= ead427046f5..061b13376ac=20100644=0A---=20a/config/programs.m4=0A+++=20= b/config/programs.m4=0A@@=20-280,7=20+280,7=20@@=20= AC_DEFUN([PGAC_CHECK_STRIP],=0A=20#=20PGAC_CHECK_LIBCURL=0A=20#=20= ------------------=0A=20#=20Check=20for=20required=20libraries=20and=20= headers,=20and=20test=20to=20see=20whether=20the=20current=0A-#=20= installation=20of=20libcurl=20is=20threadsafe.=0A+#=20installation=20of=20= libcurl=20is=20thread-safe.=0A=20=0A=20AC_DEFUN([PGAC_CHECK_LIBCURL],=0A=20= [=0A@@=20-313,7=20+313,7=20@@=20AC_DEFUN([PGAC_CHECK_LIBCURL],=0A=20=20=20= [pgac_cv__libcurl_threadsafe_init=3Dunknown])])=0A=20=20=20if=20test=20= x"$pgac_cv__libcurl_threadsafe_init"=20=3D=20xyes=20;=20then=0A=20=20=20=20= =20AC_DEFINE(HAVE_THREADSAFE_CURL_GLOBAL_INIT,=201,=0A-=20=20=20=20=20=20= =20=20=20=20=20=20=20=20[Define=20to=201=20if=20curl_global_init()=20is=20= guaranteed=20to=20be=20threadsafe.])=0A+=20=20=20=20=20=20=20=20=20=20=20= =20=20=20[Define=20to=201=20if=20curl_global_init()=20is=20guaranteed=20= to=20be=20thread-safe.])=0A=20=20=20fi=0A=20=0A=20=20=20#=20Warn=20if=20= a=20thread-friendly=20DNS=20resolver=20isn't=20built.=0Adiff=20--git=20= a/doc/src/sgml/client-auth.sgml=20b/doc/src/sgml/client-auth.sgml=0A= index=20f84085dbac4..6fc0da57f1b=20100644=0A---=20= a/doc/src/sgml/client-auth.sgml=0A+++=20b/doc/src/sgml/client-auth.sgml=0A= @@=20-2397,7=20+2397,7=20@@=20host=20...=20radius=20= radiusservers=3D"server1,server2"=20radiussecrets=3D"""secret=20one"",""=0A= =20=20=20=20=20=20=20Resource=20Server=0A=20=20=20=20=20=20=20= =0A=20=20=20=20=20=20=20=20=0A-=20=20=20=20=20=20=20=20= The=20system=20which=20hosts=20the=20protected=20resources=20which=20are=0A= +=20=20=20=20=20=20=20=20The=20system=20hosting=20the=20protected=20= resources=20which=20are=0A=20=20=20=20=20=20=20=20=20accessed=20by=20the=20= client.=20The=20PostgreSQL=0A=20=20=20=20=20=20= =20=20=20cluster=20being=20connected=20to=20is=20the=20resource=20= server.=0A=20=20=20=20=20=20=20=20=0A@@=20-2409,7=20+2409,7=20@@=20= host=20...=20radius=20radiusservers=3D"server1,server2"=20= radiussecrets=3D"""secret=20one"",""=0A=20=20=20=20=20=20=20=0A= =20=20=20=20=20=20=20=20=0A=20=20=20=20=20=20=20=20=20The=20= organization,=20product=20vendor,=20or=20other=20entity=20which=20= develops=20and/or=0A-=20=20=20=20=20=20=20=20administers=20the=20OAuth=20= servers=20and=20clients=20for=20a=20given=20application.=0A+=20=20=20=20=20= =20=20=20administers=20the=20OAuth=20resource=20servers=20and=20clients=20= for=20a=20given=20application.=0A=20=20=20=20=20=20=20=20=20Different=20= providers=20typically=20choose=20different=20implementation=20details=0A=20= =20=20=20=20=20=20=20=20for=20their=20OAuth=20systems;=20a=20client=20of=20= one=20provider=20is=20not=20generally=0A=20=20=20=20=20=20=20=20=20= guaranteed=20to=20have=20access=20to=20the=20servers=20of=20another.=0A= @@=20-2432,7=20+2432,7=20@@=20host=20...=20radius=20= radiusservers=3D"server1,server2"=20radiussecrets=3D"""secret=20one"",""=0A= =20=20=20=20=20=20=20=20=20The=20system=20which=20receives=20requests=20= from,=20and=20issues=20access=20tokens=20to,=0A=20=20=20=20=20=20=20=20=20= the=20client=20after=20the=20authenticated=20resource=20owner=20has=20= given=20approval.=0A=20=20=20=20=20=20=20=20=20= PostgreSQL=20does=20not=20provide=20an=20= authorization=0A-=20=20=20=20=20=20=20=20server;=20it's=20obtained=20= from=20the=20OAuth=20provider.=0A+=20=20=20=20=20=20=20=20server;=20it=20= is=20the=20responsibility=20of=20the=20OAuth=20provider.=0A=20=20=20=20=20= =20=20=20=0A=20=20=20=20=20=20=20=0A=20=20=20=20=20=20= =0A@@=20-2500,7=20+2500,7=20@@=20host=20...=20radius=20= radiusservers=3D"server1,server2"=20radiussecrets=3D"""secret=20one"",""=0A= =20=20=20=20=20=20=20=20=20=20exactly=20match=20the=20issuer=20= identifier=20which=20is=20provided=20in=20the=20discovery=0A=20=20=20=20=20= =20=20=20=20=20document,=20which=20must=20in=20turn=20match=20the=20= client's=0A=20=20=20=20=20=20=20=20=20=20=20setting.=20No=20variations=20= in=0A-=20=20=20=20=20=20=20=20=20case=20or=20format=20are=20permitted.=0A= +=20=20=20=20=20=20=20=20=20case=20or=20formatting=20are=20permitted.=0A=20= =20=20=20=20=20=20=20=20=0A=20=20=20=20=20=20=20=20=0A=20= =20=20=20=20=20=20=0Adiff=20--git=20= a/doc/src/sgml/installation.sgml=20b/doc/src/sgml/installation.sgml=0A= index=2096e433179b9..3c95c15a1e4=20100644=0A---=20= a/doc/src/sgml/installation.sgml=0A+++=20= b/doc/src/sgml/installation.sgml=0A@@=20-1148,8=20+1148,8=20@@=20= build-postgresql:=0A=20=20=20=20=20=20=20=20=0A=20=20=20=20=20=20= =20=20=20=0A=20=20=20=20=20=20=20=20=20=20Build=20with=20libcurl=20= support=20for=20OAuth=202.0=20client=20flows.=0A-=20=20=20=20=20=20=20=20= =20This=20requires=20the=20curl=20package=20= to=20be=0A-=20=20=20=20=20=20=20=20=20installed.=20=20Building=20with=20= this=20will=20check=20for=20the=20required=20header=20files=0A+=20=20=20=20= =20=20=20=20=20Libcurl=20version=207.61.0=20or=20later=20is=20required=20= for=20this=20feature.=0A+=20=20=20=20=20=20=20=20=20Building=20with=20= this=20will=20check=20for=20the=20required=20header=20files=0A=20=20=20=20= =20=20=20=20=20=20and=20libraries=20to=20make=20sure=20that=20your=20= curl=0A=20=20=20=20=20=20=20=20=20=20= installation=20is=20sufficient=20before=20proceeding.=0A=20=20=20=20=20=20= =20=20=20=0A@@=20-2602,9=20+2602,9=20@@=20ninja=20install=0A=20=20= =20=20=20=20=20=0A=20=20=20=20=20=20=20=20=0A=20=20=20=20= =20=20=20=20=20Build=20with=20libcurl=20support=20for=20OAuth=202.0=20= client=20flows.=0A-=20=20=20=20=20=20=20=20This=20requires=20the=20= curl=20package=20to=20be=0A-=20=20=20=20=20=20= =20=20installed.=20=20Building=20with=20this=20will=20check=20for=20the=20= required=20header=20files=0A-=20=20=20=20=20=20=20=20and=20libraries=20= to=20make=20sure=20that=20your=20curl=0A+=20=20= =20=20=20=20=20=20Libcurl=20version=207.61.0=20or=20later=20is=20= required=20for=20this=20feature.=0A+=20=20=20=20=20=20=20=20Building=20= with=20this=20will=20check=20for=20the=20required=20header=20files=0A+=20= =20=20=20=20=20=20=20and=20libraries=20to=20make=20sure=20that=20your=20= Curl=0A=20=20=20=20=20=20=20=20=20= installation=20is=20sufficient=20before=20proceeding.=20The=20default=20= for=20this=0A=20=20=20=20=20=20=20=20=20option=20is=20auto.=0A=20=20=20=20= =20=20=20=20=0Adiff=20--git=20a/doc/src/sgml/libpq.sgml=20= b/doc/src/sgml/libpq.sgml=0Aindex=20b2abae8deee..ca84226755d=20100644=0A= ---=20a/doc/src/sgml/libpq.sgml=0A+++=20b/doc/src/sgml/libpq.sgml=0A@@=20= -2390,7=20+2390,7=20@@=20postgresql://%2Fvar%2Flib%2Fpostgresql/dbname=0A= =20=20=20=20=20=20=20=20=20The=20HTTPS=20URL=20of=20a=20trusted=20issuer=20= to=20contact=20if=20the=20server=20requests=20an=0A=20=20=20=20=20=20=20=20= =20OAuth=20token=20for=20the=20connection.=20This=20parameter=20is=20= required=20for=20all=20OAuth=0A=20=20=20=20=20=20=20=20=20connections;=20= it=20should=20exactly=20match=20the=20issuer=0A-=20=20= =20=20=20=20=20=20setting=20in=20the=20= server's=20HBA=20configuration.=0A+=20=20=20=20=20=20=20=20= setting=20in=20the=20server's=20HBA=20= configuration.=0A=20=20=20=20=20=20=20=20=0A=20=20=20=20=20= =20=20=20=0A=20=20=20=20=20=20=20=20=20As=20part=20of=20the=20= standard=20authentication=20handshake,=20= libpq=0A@@=20-2399,8=20+2399,9=20@@=20= postgresql://%2Fvar%2Flib%2Fpostgresql/dbname=0A=20=20=20=20=20=20=20=20=20= provide=20a=20URL=20that=20is=20directly=20constructed=20from=20the=20= components=20of=20the=0A=20=20=20=20=20=20=20=20=20= oauth_issuer,=20and=20this=20value=20must=20exactly=20= match=20the=0A=20=20=20=20=20=20=20=20=20issuer=20identifier=20that=20is=20= declared=20in=20the=20discovery=20document=20itself,=20or=0A-=20=20=20=20= =20=20=20=20the=20connection=20will=20fail.=20This=20is=20required=20to=20= prevent=20a=20class=20of=20"mix-up=0A-=20=20=20=20=20=20=20=20attacks"=20= on=20OAuth=20clients.=0A+=20=20=20=20=20=20=20=20the=20connection=20will=20= fail.=20This=20is=20required=20to=20prevent=20a=20class=20of=0A+=20=20=20= =20=20=20=20=20=0A+=20=20=20=20=20=20=20=20"mix-up=20attacks"=20on=20= OAuth=20clients.=0A=20=20=20=20=20=20=20=20=0A=20=20=20=20=20=20=20= =20=0A=20=20=20=20=20=20=20=20=20You=20may=20also=20explicitly=20= set=20oauth_issuer=20to=20the=0A@@=20-10140,7=20= +10141,7=20@@=20void=20PQinitSSL(int=20do_ssl);=0A=20=20=20=20requests=20a=20bearer=20token=20during=0A=20= =20=20=20authentication.=20This=20flow=20can=20be=20utilized=20even=20if=20= the=20system=20running=20the=0A=20=20=20=20client=20application=20does=20= not=20have=20a=20usable=20web=20browser,=20for=20example=20when=0A-=20=20= =20running=20a=20client=20via=20SSH.=20Client=20applications=20may=20= implement=20their=20own=20flows=0A+=20=20=20running=20a=20client=20via=20= SSH.=20Client=20applications=20may=20= implement=20their=20own=20flows=0A=20=20=20=20instead;=20see=20.=0A=20=20=20=0A=20=20=20= =0A@@=20-10152,11=20+10153,11=20@@=20Visit=20= https://example.com/device=20and=20enter=20the=20code:=20ABCD-EFGH=0A=20= =0A=20=20=20=20(This=20prompt=20may=20be=0A=20=20=20=20= customized.)=0A= -=20=20=20You=20will=20then=20log=20into=20your=20OAuth=20provider,=20= which=20will=20ask=20whether=20you=20want=0A-=20=20=20to=20allow=20libpq=20= and=20the=20server=20to=20perform=20actions=20on=20your=20behalf.=20It=20= is=20always=0A+=20=20=20The=20user=20will=20then=20log=20into=20their=20= OAuth=20provider,=20which=20will=20ask=20whether=0A+=20=20=20to=20allow=20= libpq=20and=20the=20server=20to=20perform=20actions=20on=20their=20= behalf.=20It=20is=20always=0A=20=20=20=20a=20good=20idea=20to=20= carefully=20review=20the=20URL=20and=20permissions=20displayed,=20to=20= ensure=0A-=20=20=20they=20match=20your=20expectations,=20before=20= continuing.=20Do=20not=20give=20permissions=20to=0A-=20=20=20untrusted=20= third=20parties.=0A+=20=20=20they=20match=20expectations,=20before=20= continuing.=20Permissions=20should=20not=20be=20given=0A+=20=20=20to=20= untrusted=20third=20parties.=0A=20=20=20=0A=20=20=20=0A=20=20= =20=20For=20an=20OAuth=20client=20flow=20to=20be=20usable,=20the=20= connection=20string=20must=20at=20minimum=0A@@=20-10199,7=20+10200,7=20= @@=20void=20PQsetAuthDataHook(PQauthDataHook_type=20hook);=0A=20= =0A=20int=20hook_fn(PGauthData=20type,=20PGconn=20*conn,=20= void=20*data);=0A=20=0A-=20=20=20=20=20=20=20=20which=20= libpq=20will=20call=20when=20when=20action=20= is=0A+=20=20=20=20=20=20=20=20which=20libpq=20= will=20call=20when=20an=20action=20is=0A=20=20=20=20=20=20=20=20=20= required=20of=20the=20application.=20type=20= describes=0A=20=20=20=20=20=20=20=20=20the=20request=20being=20made,=20= conn=20is=20the=0A=20=20=20=20=20=20=20=20=20= connection=20handle=20being=20authenticated,=20and=20= data=0A@@=20-10431,7=20+10432,7=20@@=20= typedef=20struct=20_PGoauthBearerRequest=0A=20=20=20=20=20=20=0A= =20=20=20=20=20=20=0A=20=20=20=20=20=20=20=0A-=20=20=20=20= =20=20=20sprays=20HTTP=20traffic=20(containing=20several=20critical=20= secrets)=20to=20standard=0A+=20=20=20=20=20=20=20prints=20HTTP=20traffic=20= (containing=20several=20critical=20secrets)=20to=20standard=0A=20=20=20=20= =20=20=20=20error=20during=20the=20OAuth=20flow=0A=20=20=20=20=20=20=20= =0A=20=20=20=20=20=20=0A@@=20-10526,13=20+10527,13=20= @@=20int=20PQisthreadsafe();=0A=20=20=20=0A=20=0A=20=20=20=0A= -=20=20=20Similarly,=20if=20you=20are=20using=20Curl=20inside=20your=20= application,=0A+=20=20=20Similarly,=20if=20you=20are=20using=20= Curl=20inside=20your=20= application,=0A=20=20=20=20and=20you=20do=20not=20= already=0A=20=20=20=20initialize=0A=20=20= =20=20libcurl=20globally=20before=20starting=20new=20threads,=20= you=20will=20need=20to=0A=20=20=20=20cooperatively=20lock=20(again=20via=20= PQregisterThreadLock)=0A=20=20=20=20around=20any=20= code=20that=20may=20initialize=20libcurl.=20This=20restriction=20is=20= lifted=20for=0A-=20=20=20more=20recent=20versions=20of=20Curl=20that=20= are=20built=20to=20support=20threadsafe=0A+=20=20=20more=20recent=20= versions=20of=20Curl=20that=20are=20built=20= to=20support=20thread-safe=0A=20=20=20=20initialization;=20those=20= builds=20can=20be=20identified=20by=20the=20advertisement=20of=20a=0A=20=20= =20=20threadsafe=20feature=20in=20their=20version=20= metadata.=0A=20=20=20=0Adiff=20--git=20= a/doc/src/sgml/oauth-validators.sgml=20= b/doc/src/sgml/oauth-validators.sgml=0Aindex=20eb8c4431c2d..e9d28d3daea=20= 100644=0A---=20a/doc/src/sgml/oauth-validators.sgml=0A+++=20= b/doc/src/sgml/oauth-validators.sgml=0A@@=20-10,8=20+10,8=20@@=0A=20=20=20= custom=20modules=20to=20perform=20server-side=20validation=20of=20OAuth=20= bearer=20tokens.=0A=20=20=20Because=20OAuth=20implementations=20vary=20= so=20wildly,=20and=20bearer=20token=20validation=20is=0A=20=20=20heavily=20= dependent=20on=20the=20issuing=20party,=20the=20server=20cannot=20check=20= the=20token=0A-=20=20itself;=20validator=20modules=20provide=20the=20= glue=20between=20the=20server=20and=20the=20OAuth=0A-=20=20provider=20in=20= use.=0A+=20=20itself;=20validator=20modules=20provide=20the=20= integration=20layer=20between=20the=20server=0A+=20=20and=20the=20OAuth=20= provider=20in=20use.=0A=20=20=0A=20=20=0A=20=20=20OAuth=20= validator=20modules=20must=20at=20least=20consist=20of=20an=20= initialization=20function=0A@@=20-21,7=20+21,7=20@@=0A=20=20=0A=20= =20=20=0A=20=20=20=20Since=20a=20misbehaving=20validator=20might=20= let=20unauthorized=20users=20into=20the=20database,=0A-=20=20=20correct=20= implementation=20is=20critical.=20See=0A+=20=20=20validating=20the=20= correctness=20of=20the=20implementation=20is=20critical.=20See=0A=20=20=20= =20=20for=20design=20= considerations.=0A=20=20=20=0A=20=20=0A@@=20-290,8=20= +290,9=20@@=0A=20=20=20=20= _PG_oauth_validator_module_init=0A=20=20=20= =0A=20=20=20=0A-=20=20=20An=20OAuth=20validator=20= module=20is=20loaded=20by=20dynamically=20loading=20one=20of=20the=20= shared=0A+=20=20=20OAuth=20validator=20modules=20are=20dynamically=20= loaded=20from=20the=20shared=0A=20=20=20=20libraries=20listed=20in=20= .=0A+=20=20=20Modules=20= are=20loaded=20on=20demand=20when=20requested=20from=20a=20login=20in=20= progress.=0A=20=20=20=20The=20normal=20library=20search=20path=20is=20= used=20to=20locate=20the=20library.=20To=0A=20=20=20=20provide=20the=20= validator=20callbacks=20and=20to=20indicate=20that=20the=20library=20is=20= an=20OAuth=0A=20=20=20=20validator=20module=20a=20function=20named=0A@@=20= -356,7=20+357,7=20@@=20typedef=20bool=20(*ValidatorValidateCB)=20(const=20= ValidatorModuleState=20*state,=0A=20=0A=20=0A=20=20=20=20= =20token=20will=20contain=20the=20bearer=20= token=20to=20validate.=0A-=20=20=20=20The=20server=20has=20ensured=20= that=20the=20token=20is=20well-formed=20syntactically,=20but=20no=0A+=20=20= =20=20libpq=20has=20ensured=20that=20the=20= token=20is=20well-formed=20syntactically,=20but=20no=0A=20=20=20=20=20= other=20validation=20has=20been=20performed.=20=20= role=20will=0A=20=20=20=20=20contain=20the=20= role=20the=20user=20has=20requested=20to=20log=20in=20as.=20=20The=20= callback=20must=0A=20=20=20=20=20set=20output=20parameters=20in=20the=20= result=20struct,=20which=20is=0A@@=20-371,10=20= +372,10=20@@=20typedef=20struct=20ValidatorModuleResult=0A=20= =0A=20=0A=20=20=20=20=20The=20connection=20will=20only=20= proceed=20if=20the=20module=20sets=0A-=20=20=20=20= authorized=20to=20true.=20=20= To=0A+=20=20=20=20result->authorized=20to=20= true.=20=20To=0A=20=20=20=20=20authenticate=20the=20= user,=20the=20authenticated=20user=20name=20(as=20determined=20using=20= the=0A-=20=20=20=20token)=20shall=20be=20palloc'd=20and=20returned=20in=20= the=20authn_id=0A-=20=20=20=20field.=20=20= Alternatively,=20authn_id=20may=20be=20set=20= to=0A+=20=20=20=20token)=20shall=20be=20palloc'd=20and=20returned=20in=20= the=20result->authn_id=0A+=20=20=20=20field.=20= =20Alternatively,=20result->authn_id=20may=20= be=20set=20to=0A=20=20=20=20=20NULL=20if=20the=20token=20is=20valid=20= but=20the=20associated=20user=20identity=20cannot=20be=0A=20=20=20=20=20= determined.=0A=20=20=20=20=0A@@=20-386,12=20+387,12=20@@=20= typedef=20struct=20ValidatorModuleResult=0A=20=20=20=20=0A=20=20=20= =20=0A=20=20=20=20=20The=20behavior=20after=20= validate_cb=20returns=20depends=20on=20the=0A-=20=20= =20=20specific=20HBA=20setup.=20=20Normally,=20the=20= authn_id=20user=0A+=20=20=20=20specific=20HBA=20= setup.=20=20Normally,=20the=20= result->authn_id=20user=0A=20=20=20=20=20name=20= must=20exactly=20match=20the=20role=20that=20the=20user=20is=20logging=20= in=20as.=20=20(This=0A=20=20=20=20=20behavior=20may=20be=20modified=20= with=20a=20usermap.)=20=20But=20when=20authenticating=20against=0A-=20=20= =20=20an=20HBA=20rule=20with=20delegate_ident_mapping=20= turned=20on,=20the=0A-=20=20=20=20server=20will=20not=20perform=20any=20= checks=20on=20the=20value=20of=0A-=20=20=20=20= authn_id=20at=20all;=20in=20this=20case=20it=20= is=20up=20to=20the=0A+=20=20=20=20an=20HBA=20rule=20with=20= delegate_ident_mapping=20turned=20on,=0A+=20=20=20=20= PostgreSQL=20will=20not=20perform=20any=20= checks=20on=20the=20value=20of=0A+=20=20=20=20= result->authn_id=20at=20all;=20in=20this=20= case=20it=20is=20up=20to=20the=0A=20=20=20=20=20validator=20to=20ensure=20= that=20the=20token=20carries=20enough=20privileges=20for=20the=20user=20= to=0A=20=20=20=20=20log=20in=20under=20the=20indicated=20= role.=0A=20=20=20=20=0A@@=20-402,7=20= +403,7=20@@=20typedef=20struct=20ValidatorModuleResult=0A=20=20=20=20= =0A=20=20=20=20=20The=20shutdown_cb=20= callback=20is=20executed=20when=20the=20backend=0A=20=20=20=20=20process=20= associated=20with=20the=20connection=20exits.=20If=20the=20validator=20= module=20has=0A-=20=20=20=20any=20state,=20this=20callback=20should=20= free=20it=20to=20avoid=20resource=20leaks.=0A+=20=20=20=20any=20= allocated=20state,=20this=20callback=20should=20free=20it=20to=20avoid=20= resource=20leaks.=0A=20=0A=20typedef=20void=20= (*ValidatorShutdownCB)=20(ValidatorModuleState=20*state);=0A=20= =0Adiff=20--git=20a/meson.build=20b/meson.build=0Aindex=20= 5c35159b4f1..574f992ed49=20100644=0A---=20a/meson.build=0A+++=20= b/meson.build=0A@@=20-867,7=20+867,7=20@@=20if=20not=20= libcurlopt.disabled()=0A=20=20=20if=20libcurl.found()=0A=20=20=20=20=20= cdata.set('USE_LIBCURL',=201)=0A=20=0A-=20=20=20=20#=20Check=20to=20see=20= whether=20the=20current=20platform=20supports=20threadsafe=20Curl=0A+=20=20= =20=20#=20Check=20to=20see=20whether=20the=20current=20platform=20= supports=20thread-safe=20Curl=0A=20=20=20=20=20#=20initialization.=0A=20=20= =20=20=20libcurl_threadsafe_init=20=3D=20false=0A=20=0A@@=20-897,11=20= +897,11=20@@=20if=20not=20libcurlopt.disabled()=0A=20=20=20=20=20=20=20= assert(r.compiled())=0A=20=20=20=20=20=20=20if=20r.returncode()=20=3D=3D=20= 0=0A=20=20=20=20=20=20=20=20=20libcurl_threadsafe_init=20=3D=20true=0A-=20= =20=20=20=20=20=20=20message('curl_global_init=20is=20threadsafe')=0A+=20= =20=20=20=20=20=20=20message('curl_global_init=20is=20thread-safe')=0A=20= =20=20=20=20=20=20elif=20r.returncode()=20=3D=3D=201=0A-=20=20=20=20=20=20= =20=20message('curl_global_init=20is=20not=20threadsafe')=0A+=20=20=20=20= =20=20=20=20message('curl_global_init=20is=20not=20thread-safe')=0A=20=20= =20=20=20=20=20else=0A-=20=20=20=20=20=20=20=20message('curl_global_init=20= failed;=20assuming=20not=20threadsafe')=0A+=20=20=20=20=20=20=20=20= message('curl_global_init=20failed;=20assuming=20not=20thread-safe')=0A=20= =20=20=20=20=20=20endif=0A=20=20=20=20=20endif=0A=20=0Adiff=20--git=20= a/src/backend/libpq/auth-oauth.c=20b/src/backend/libpq/auth-oauth.c=0A= index=20e2b5d1ed913..db56cd8c200=20100644=0A---=20= a/src/backend/libpq/auth-oauth.c=0A+++=20= b/src/backend/libpq/auth-oauth.c=0A@@=20-4,9=20+4,9=20@@=0A=20=20*=09=20=20= Server-side=20implementation=20of=20the=20SASL=20OAUTHBEARER=20= mechanism.=0A=20=20*=0A=20=20*=20See=20the=20following=20RFC=20for=20= more=20details:=0A-=20*=20-=20RFC=207628:=20= https://tools.ietf.org/html/rfc7628=0A+=20*=20-=20RFC=207628:=20= https://datatracker.ietf.org/doc/html/rfc7628=0A=20=20*=0A-=20*=20= Portions=20Copyright=20(c)=201996-2024,=20PostgreSQL=20Global=20= Development=20Group=0A+=20*=20Portions=20Copyright=20(c)=201996-2025,=20= PostgreSQL=20Global=20Development=20Group=0A=20=20*=20Portions=20= Copyright=20(c)=201994,=20Regents=20of=20the=20University=20of=20= California=0A=20=20*=0A=20=20*=20src/backend/libpq/auth-oauth.c=0A@@=20= -70,7=20+70,7=20@@=20struct=20oauth_ctx=0A=20=09const=20char=20*scope;=0A= =20};=0A=20=0A-static=20char=20*sanitize_char(char=20c);=0A+static=20= void=20sanitize_char(char=20c,=20char=20*buf,=20size_t=20buflen);=0A=20= static=20char=20*parse_kvpairs_for_auth(char=20**input);=0A=20static=20= void=20generate_error_response(struct=20oauth_ctx=20*ctx,=20char=20= **output,=20int=20*outputlen);=0A=20static=20bool=20validate(Port=20= *port,=20const=20char=20*auth);=0A@@=20-139,6=20+139,7=20@@=20= oauth_exchange(void=20*opaq,=20const=20char=20*input,=20int=20inputlen,=0A= =20=09char=09=09cbind_flag;=0A=20=09char=09=20=20=20*auth;=0A=20=09int=09= =09=09status;=0A+=09char=09=09errmsgbuf[5];=0A=20=0A=20=09struct=20= oauth_ctx=20*ctx=20=3D=20opaq;=0A=20=0A@@=20-162,6=20+163,7=20@@=20= oauth_exchange(void=20*opaq,=20const=20char=20*input,=20int=20inputlen,=0A= =20=0A=20=09/*=0A=20=09=20*=20Check=20that=20the=20input=20length=20= agrees=20with=20the=20string=20length=20of=20the=20input.=0A+=09=20*=20= Possible=20reasons=20for=20discrepancies=20include=20embedded=20nulls=20= in=20the=20string.=0A=20=09=20*/=0A=20=09if=20(inputlen=20=3D=3D=200)=0A=20= =09=09ereport(ERROR,=0A@@=20-223,22=20+225,29=20@@=20oauth_exchange(void=20= *opaq,=20const=20char=20*input,=20int=20inputlen,=0A=20=0A=20=09=09case=20= 'y':=09=09=09=09/*=20fall=20through=20*/=0A=20=09=09case=20'n':=0A-=09=09= =09p++;=0A+=09=09=09if=20(!*(++p))=0A+=09=09=09=09goto=20endofmessage;=0A= +=0A=20=09=09=09if=20(*p=20!=3D=20',')=0A+=09=09=09{=0A+=09=09=09=09= sanitize_char(*p,=20errmsgbuf,=20sizeof(errmsgbuf));=0A=20=09=09=09=09= ereport(ERROR,=0A=20=09=09=09=09=09=09= errcode(ERRCODE_PROTOCOL_VIOLATION),=0A=20=09=09=09=09=09=09= errmsg("malformed=20OAUTHBEARER=20message"),=0A=20=09=09=09=09=09=09= errdetail("Comma=20expected,=20but=20found=20character=20\"%s\".",=0A-=09= =09=09=09=09=09=09=09=20=20sanitize_char(*p)));=0A-=09=09=09p++;=0A+=09=09= =09=09=09=09=09=09=20=20errmsgbuf));=0A+=09=09=09}=0A+=09=09=09if=20= (!*(++p))=0A+=09=09=09=09goto=20endofmessage;=0A=20=09=09=09break;=0A=20=0A= =20=09=09default:=0A+=09=09=09sanitize_char(*p,=20errmsgbuf,=20= sizeof(errmsgbuf));=0A=20=09=09=09ereport(ERROR,=0A=20=09=09=09=09=09= errcode(ERRCODE_PROTOCOL_VIOLATION),=0A=20=09=09=09=09=09= errmsg("malformed=20OAUTHBEARER=20message"),=0A=20=09=09=09=09=09= errdetail("Unexpected=20channel-binding=20flag=20\"%s\".",=0A-=09=09=09=09= =09=09=09=20=20sanitize_char(cbind_flag)));=0A+=09=09=09=09=09=09=09=20=20= errmsgbuf));=0A=20=09}=0A=20=0A=20=09/*=0A@@=20-249,21=20+258,29=20@@=20= oauth_exchange(void=20*opaq,=20const=20char=20*input,=20int=20inputlen,=0A= =20=09=09=09=09errcode(ERRCODE_FEATURE_NOT_SUPPORTED),=0A=20=09=09=09=09= errmsg("client=20uses=20authorization=20identity,=20but=20it=20is=20not=20= supported"));=0A=20=09if=20(*p=20!=3D=20',')=0A+=09{=0A+=09=09= sanitize_char(*p,=20errmsgbuf,=20sizeof(errmsgbuf));=0A=20=09=09= ereport(ERROR,=0A=20=09=09=09=09errcode(ERRCODE_PROTOCOL_VIOLATION),=0A=20= =09=09=09=09errmsg("malformed=20OAUTHBEARER=20message"),=0A=20=09=09=09=09= errdetail("Unexpected=20attribute=20\"%s\"=20in=20= client-first-message.",=0A-=09=09=09=09=09=09=20=20sanitize_char(*p)));=0A= -=09p++;=0A+=09=09=09=09=09=09=20=20errmsgbuf));=0A+=09}=0A+=09if=20= (!*(++p))=0A+=09=09goto=20endofmessage;=0A=20=0A=20=09/*=20All=20= remaining=20fields=20are=20separated=20by=20the=20RFC's=20kvsep=20= (\x01).=20*/=0A=20=09if=20(*p=20!=3D=20KVSEP)=0A+=09{=0A+=09=09= sanitize_char(*p,=20errmsgbuf,=20sizeof(errmsgbuf));=0A=20=09=09= ereport(ERROR,=0A=20=09=09=09=09errcode(ERRCODE_PROTOCOL_VIOLATION),=0A=20= =09=09=09=09errmsg("malformed=20OAUTHBEARER=20message"),=0A=20=09=09=09=09= errdetail("Key-value=20separator=20expected,=20but=20found=20character=20= \"%s\".",=0A-=09=09=09=09=09=09=20=20sanitize_char(*p)));=0A-=09p++;=0A+=09= =09=09=09=09=09=20=20errmsgbuf));=0A+=09}=0A+=09if=20(!*(++p))=0A+=09=09= goto=20endofmessage;=0A=20=0A=20=09auth=20=3D=20= parse_kvpairs_for_auth(&p);=0A=20=09if=20(!auth)=0A@@=20-296,6=20+313,13=20= @@=20oauth_exchange(void=20*opaq,=20const=20char=20*input,=20int=20= inputlen,=0A=20=09explicit_bzero(input_copy,=20inputlen);=0A=20=0A=20=09= return=20status;=0A+=0A+endofmessage:=0A+=09explicit_bzero(input_copy,=20= inputlen);=0A+=09ereport(ERROR,=0A+=09=09=09= errcode(ERRCODE_PROTOCOL_VIOLATION),=0A+=09=09=09errmsg("malformed=20= OAUTHBEARER=20message"));=0A+=09pg_unreachable();=0A=20}=0A=20=0A=20/*=0A= @@=20-303,19=20+327,14=20@@=20oauth_exchange(void=20*opaq,=20const=20= char=20*input,=20int=20inputlen,=0A=20=20*=0A=20=20*=20If=20it's=20a=20= printable=20ASCII=20character,=20print=20it=20as=20a=20single=20= character.=0A=20=20*=20otherwise,=20print=20it=20in=20hex.=0A-=20*=0A-=20= *=20The=20returned=20pointer=20points=20to=20a=20static=20buffer.=0A=20=20= */=0A-static=20char=20*=0A-sanitize_char(char=20c)=0A+static=20void=0A= +sanitize_char(char=20c,=20char=20*buf,=20size_t=20buflen)=0A=20{=0A-=09= static=20char=20buf[5];=0A-=0A=20=09if=20(c=20>=3D=200x21=20&&=20c=20<=3D=20= 0x7E)=0A-=09=09snprintf(buf,=20sizeof(buf),=20"'%c'",=20c);=0A+=09=09= snprintf(buf,=20buflen,=20"'%c'",=20c);=0A=20=09else=0A-=09=09= snprintf(buf,=20sizeof(buf),=20"0x%02x",=20(unsigned=20char)=20c);=0A-=09= return=20buf;=0A+=09=09snprintf(buf,=20buflen,=20"0x%02x",=20(unsigned=20= char)=20c);=0A=20}=0A=20=0A=20/*=0A@@=20-660,7=20+679,9=20@@=20= validate(Port=20*port,=20const=20char=20*auth)=0A=20=09if=20= (!ValidatorCallbacks->validate_cb(validator_module_state,=20token,=0A=20=09= =09=09=09=09=09=09=09=09=09=20port->user_name,=20ret))=0A=20=09{=0A-=09=09= ereport(LOG,=20errmsg("internal=20error=20in=20OAuth=20validator=20= module"));=0A+=09=09ereport(WARNING,=0A+=09=09=09=09= errcode(ERRCODE_INTERNAL_ERROR),=0A+=09=09=09=09errmsg("internal=20error=20= in=20OAuth=20validator=20module"));=0A=20=09=09return=20false;=0A=20=09}=0A= =20=0A@@=20-738,6=20+759,11=20@@=20load_validator_library(const=20char=20= *libname)=0A=20=09OAuthValidatorModuleInit=20validator_init;=0A=20=09= MemoryContextCallback=20*mcb;=0A=20=0A+=09/*=0A+=09=20*=20Thre=20= presence,=20and=20validity,=20of=20libname=20has=20already=20been=20= established=20by=0A+=09=20*=20check_oauth_validator=20so=20we=20don't=20= need=20to=20perform=20more=20than=20Assert=20level=0A+=09=20*=20checking=20= here.=0A+=09=20*/=0A=20=09Assert(libname=20&&=20*libname);=0A=20=0A=20=09= validator_init=20=3D=20(OAuthValidatorModuleInit)=0A@@=20-768,6=20= +794,15=20@@=20load_validator_library(const=20char=20*libname)=0A=20=09=09= =09=09errdetail("Server=20has=20magic=20number=200x%08X,=20module=20has=20= 0x%08X.",=0A=20=09=09=09=09=09=09=20=20PG_OAUTH_VALIDATOR_MAGIC,=20= ValidatorCallbacks->magic));=0A=20=0A+=09/*=0A+=09=20*=20Make=20sure=20= all=20required=20callbacks=20are=20present=20in=20the=20= ValidatorCallbacks=0A+=09=20*=20structure.=20Right=20now=20only=20the=20= validation=20callback=20is=20required.=0A+=09=20*/=0A+=09if=20= (ValidatorCallbacks->validate_cb=20=3D=3D=20NULL)=0A+=09=09= ereport(ERROR,=0A+=09=09=09=09errmsg("%s=20module=20\"%s\"=20must=20= define=20the=20symbol=20%s",=0A+=09=09=09=09=09=20=20=20"OAuth=20= validator",=20libname,=20"validate_cb"));=0A+=0A=20=09/*=20Allocate=20= memory=20for=20validator=20library=20private=20state=20data=20*/=0A=20=09= validator_module_state=20=3D=20(ValidatorModuleState=20*)=20= palloc0(sizeof(ValidatorModuleState));=0A=20=09= validator_module_state->sversion=20=3D=20PG_VERSION_NUM;=0A@@=20-804,7=20= +839,7=20@@=20bool=0A=20check_oauth_validator(HbaLine=20*hbaline,=20int=20= elevel,=20char=20**err_msg)=0A=20{=0A=20=09int=09=09=09line_num=20=3D=20= hbaline->linenumber;=0A-=09char=09=20=20=20*file_name=20=3D=20= hbaline->sourcefile;=0A+=09const=20char=20*file_name=20=3D=20= hbaline->sourcefile;=0A=20=09char=09=20=20=20*rawstring;=0A=20=09List=09=20= =20=20*elemlist=20=3D=20NIL;=0A=20=0Adiff=20--git=20= a/src/include/common/oauth-common.h=20= b/src/include/common/oauth-common.h=0Aindex=208fe56267780..5fb559d84b2=20= 100644=0A---=20a/src/include/common/oauth-common.h=0A+++=20= b/src/include/common/oauth-common.h=0A@@=20-3,7=20+3,7=20@@=0A=20=20*=20= oauth-common.h=0A=20=20*=09=09Declarations=20for=20helper=20functions=20= used=20for=20OAuth/OIDC=20authentication=0A=20=20*=0A-=20*=20Portions=20= Copyright=20(c)=201996-2024,=20PostgreSQL=20Global=20Development=20Group=0A= +=20*=20Portions=20Copyright=20(c)=201996-2025,=20PostgreSQL=20Global=20= Development=20Group=0A=20=20*=20Portions=20Copyright=20(c)=201994,=20= Regents=20of=20the=20University=20of=20California=0A=20=20*=0A=20=20*=20= src/include/common/oauth-common.h=0Adiff=20--git=20= a/src/include/libpq/oauth.h=20b/src/include/libpq/oauth.h=0Aindex=20= 7e249613e10..2f01b669633=20100644=0A---=20a/src/include/libpq/oauth.h=0A= +++=20b/src/include/libpq/oauth.h=0A@@=20-3,7=20+3,7=20@@=0A=20=20*=20= oauth.h=0A=20=20*=09=20=20Interface=20to=20libpq/auth-oauth.c=0A=20=20*=0A= -=20*=20Portions=20Copyright=20(c)=201996-2024,=20PostgreSQL=20Global=20= Development=20Group=0A+=20*=20Portions=20Copyright=20(c)=201996-2025,=20= PostgreSQL=20Global=20Development=20Group=0A=20=20*=20Portions=20= Copyright=20(c)=201994,=20Regents=20of=20the=20University=20of=20= California=0A=20=20*=0A=20=20*=20src/include/libpq/oauth.h=0A@@=20-83,8=20= +83,9=20@@=20typedef=20struct=20OAuthValidatorCallbacks=0A=20}=20= OAuthValidatorCallbacks;=0A=20=0A=20/*=0A-=20*=20Type=20of=20the=20= shared=20library=20symbol=20_PG_oauth_validator_module_init=20that=20is=0A= -=20*=20looked=20up=20when=20loading=20a=20validator=20module.=0A+=20*=20= Type=20of=20the=20shared=20library=20symbol=20= _PG_oauth_validator_module_init=20which=20is=0A+=20*=20required=20for=20= all=20validator=20modules.=20=20This=20function=20will=20be=20invoked=20= during=0A+=20*=20module=20loading.=0A=20=20*/=0A=20typedef=20const=20= OAuthValidatorCallbacks=20*(*OAuthValidatorModuleInit)=20(void);=0A=20= extern=20PGDLLEXPORT=20const=20OAuthValidatorCallbacks=20= *_PG_oauth_validator_module_init(void);=0Adiff=20--git=20= a/src/include/pg_config.h.in=20b/src/include/pg_config.h.in=0Aindex=20= c04ee38d086..db6454090d2=20100644=0A---=20a/src/include/pg_config.h.in=0A= +++=20b/src/include/pg_config.h.in=0A@@=20-445,7=20+445,7=20@@=0A=20/*=20= Define=20to=201=20if=20you=20have=20the=20=20header=20file.=20= */=0A=20#undef=20HAVE_TERMIOS_H=0A=20=0A-/*=20Define=20to=201=20if=20= curl_global_init()=20is=20guaranteed=20to=20be=20threadsafe.=20*/=0A+/*=20= Define=20to=201=20if=20curl_global_init()=20is=20guaranteed=20to=20be=20= thread-safe.=20*/=0A=20#undef=20HAVE_THREADSAFE_CURL_GLOBAL_INIT=0A=20=0A= =20/*=20Define=20to=201=20if=20your=20compiler=20understands=20`typeof'=20= or=20something=20similar.=20*/=0Adiff=20--git=20= a/src/interfaces/libpq/fe-auth-oauth-curl.c=20= b/src/interfaces/libpq/fe-auth-oauth-curl.c=0Aindex=20= 74323de309a..c9aa51b1007=20100644=0A---=20= a/src/interfaces/libpq/fe-auth-oauth-curl.c=0A+++=20= b/src/interfaces/libpq/fe-auth-oauth-curl.c=0A@@=20-4,7=20+4,7=20@@=0A=20= =20*=09=20=20=20The=20libcurl=20implementation=20of=20OAuth/OIDC=20= authentication,=20using=20the=0A=20=20*=09=20=20=20OAuth=20Device=20= Authorization=20Grant=20(RFC=208628).=0A=20=20*=0A-=20*=20Portions=20= Copyright=20(c)=201996-2024,=20PostgreSQL=20Global=20Development=20Group=0A= +=20*=20Portions=20Copyright=20(c)=201996-2025,=20PostgreSQL=20Global=20= Development=20Group=0A=20=20*=20Portions=20Copyright=20(c)=201994,=20= Regents=20of=20the=20University=20of=20California=0A=20=20*=0A=20=20*=20= IDENTIFICATION=0A@@=20-917,7=20+917,7=20@@=20parse_interval(struct=20= async_ctx=20*actx,=20const=20char=20*interval_str)=0A=20=09if=20(parsed=20= <=201)=0A=20=09=09return=20actx->debugging=20?=200=20:=201;=0A=20=0A-=09= else=20if=20(INT_MAX=20<=3D=20parsed)=0A+=09else=20if=20(parsed=20>=3D=20= INT_MAX)=0A=20=09=09return=20INT_MAX;=0A=20=0A=20=09return=20parsed;=0A= @@=20-940,7=20+940,7=20@@=20parse_expires_in(struct=20async_ctx=20*actx,=20= const=20char=20*expires_in_str)=0A=20=09parsed=20=3D=20= parse_json_number(expires_in_str);=0A=20=09parsed=20=3D=20round(parsed);=0A= =20=0A-=09if=20(INT_MAX=20<=3D=20parsed)=0A+=09if=20(parsed=20>=3D=20= INT_MAX)=0A=20=09=09return=20INT_MAX;=0A=20=09else=20if=20(parsed=20<=3D=20= INT_MIN)=0A=20=09=09return=20INT_MIN;=0A@@=20-966,6=20+966,10=20@@=20= parse_device_authz(struct=20async_ctx=20*actx,=20struct=20device_authz=20= *authz)=0A=20=09=09=20*/=0A=20=09=09{"verification_url",=20= JSON_TOKEN_STRING,=20{&authz->verification_uri},=20REQUIRED},=0A=20=0A+=09= =09/*=0A+=09=09=20*=20There=20is=20no=20evidence=20of=20= verification_uri_complete=20being=20spelled=0A+=09=09=20*=20with=20"url"=20= instead=20with=20any=20service=20provider,=20so=20only=20support=20= "uri".=0A+=09=09=20*/=0A=20=09=09{"verification_uri_complete",=20= JSON_TOKEN_STRING,=20{&authz->verification_uri_complete},=20OPTIONAL},=0A= =20=09=09{"interval",=20JSON_TOKEN_NUMBER,=20{&authz->interval_str},=20= OPTIONAL},=0A=20=0A@@=20-1870,6=20+1874,7=20@@=20= append_urlencoded(PQExpBuffer=20buf,=20const=20char=20*s)=0A=20=09char=09= =20=20=20*haystack;=0A=20=09char=09=20=20=20*match;=0A=20=0A+=09/*=20The=20= first=20parameter=20to=20curl_easy_escape=20is=20deprecated=20by=20Curl=20= */=0A=20=09escaped=20=3D=20curl_easy_escape(NULL,=20s,=200);=0A=20=09if=20= (!escaped)=0A=20=09{=0A@@=20-2273,6=20+2278,7=20@@=20= finish_device_authz(struct=20async_ctx=20*actx)=0A=20=09=09=09return=20= false;=0A=20=09=09}=0A=20=0A+=09=09/*=20Copy=20the=20token=20error=20= into=20the=20context=20error=20buffer=20*/=0A=20=09=09= record_token_error(actx,=20&err);=0A=20=0A=20=09=09= free_token_error(&err);=0A@@=20-2541,7=20+2547,7=20@@=20= initialize_curl(PGconn=20*conn)=0A=20=0A=20=09/*=0A=20=09=20*=20If=20we=20= determined=20at=20configure=20time=20that=20the=20Curl=20installation=20= is=0A-=09=20*=20threadsafe,=20our=20job=20here=20is=20much=20easier.=20= We=20simply=20initialize=20above=0A+=09=20*=20thread-safe,=20our=20job=20= here=20is=20much=20easier.=20We=20simply=20initialize=20above=0A=20=09=20= *=20without=20any=20locking=20(concurrent=20or=20duplicated=20calls=20= are=20fine=20in=20that=0A=20=09=20*=20situation),=20then=20double-check=20= to=20make=20sure=20the=20runtime=20setting=20agrees,=0A=20=09=20*=20to=20= try=20to=20catch=20silent=20downgrades.=0A@@=20-2553,8=20+2559,8=20@@=20= initialize_curl(PGconn=20*conn)=0A=20=09=09=20*=20In=20a=20downgrade=20= situation,=20the=20damage=20is=20already=20done.=20Curl=20global=0A=20=09= =09=20*=20state=20may=20be=20corrupted.=20Be=20noisy.=0A=20=09=09=20*/=0A= -=09=09libpq_append_conn_error(conn,=20"libcurl=20is=20no=20longer=20= threadsafe\n"=0A-=09=09=09=09=09=09=09=09"\tCurl=20initialization=20was=20= reported=20threadsafe=20when=20libpq\n"=0A+=09=09= libpq_append_conn_error(conn,=20"libcurl=20is=20no=20longer=20= thread-safe\n"=0A+=09=09=09=09=09=09=09=09"\tCurl=20initialization=20was=20= reported=20thread-safe=20when=20libpq\n"=0A=20=09=09=09=09=09=09=09=09= "\twas=20compiled,=20but=20the=20currently=20installed=20version=20of\n"=0A= =20=09=09=09=09=09=09=09=09"\tlibcurl=20reports=20that=20it=20is=20not.=20= Recompile=20libpq=20against\n"=0A=20=09=09=09=09=09=09=09=09"\tthe=20= installed=20version=20of=20libcurl.");=0Adiff=20--git=20= a/src/interfaces/libpq/fe-auth-oauth.c=20= b/src/interfaces/libpq/fe-auth-oauth.c=0Aindex=20= 8beae9604c7..24448c3e209=20100644=0A---=20= a/src/interfaces/libpq/fe-auth-oauth.c=0A+++=20= b/src/interfaces/libpq/fe-auth-oauth.c=0A@@=20-4,7=20+4,7=20@@=0A=20=20*=09= =20=20=20The=20front-end=20(client)=20implementation=20of=20OAuth/OIDC=20= authentication=0A=20=20*=09=20=20=20using=20the=20SASL=20OAUTHBEARER=20= mechanism.=0A=20=20*=0A-=20*=20Portions=20Copyright=20(c)=201996-2024,=20= PostgreSQL=20Global=20Development=20Group=0A+=20*=20Portions=20Copyright=20= (c)=201996-2025,=20PostgreSQL=20Global=20Development=20Group=0A=20=20*=20= Portions=20Copyright=20(c)=201994,=20Regents=20of=20the=20University=20= of=20California=0A=20=20*=0A=20=20*=20IDENTIFICATION=0A@@=20-272,7=20= +272,7=20@@=20oauth_json_scalar(void=20*state,=20char=20*token,=20= JsonTokenType=20type)=0A=20=09=09=09=20*=20Assert=20and=20don't=20= continue=20any=20further=20for=20production=20builds.=0A=20=09=09=09=20= */=0A=20=09=09=09Assert(false);=0A-=09=09=09oauth_json_set_error(ctx,=09= /*=20don't=20bother=20translating=20*/=0A+=09=09=09= oauth_json_set_error(ctx,=0A=20=09=09=09=09=09=09=09=09=20"internal=20= error:=20target=20scalar=20found=20at=20nesting=20level=20%d=20during=20= OAUTHBEARER=20parsing",=0A=20=09=09=09=09=09=09=09=09=20ctx->nested);=0A=20= =09=09=09return=20JSON_SEM_ACTION_FAILED;=0Adiff=20--git=20= a/src/test/modules/oauth_validator/Makefile=20= b/src/test/modules/oauth_validator/Makefile=0Aindex=20= f297ed5c968..bbd2a98023b=20100644=0A---=20= a/src/test/modules/oauth_validator/Makefile=0A+++=20= b/src/test/modules/oauth_validator/Makefile=0A@@=20-2,7=20+2,7=20@@=0A=20= #=0A=20#=20Makefile=20for=20src/test/modules/oauth_validator=0A=20#=0A-#=20= Portions=20Copyright=20(c)=201996-2024,=20PostgreSQL=20Global=20= Development=20Group=0A+#=20Portions=20Copyright=20(c)=201996-2025,=20= PostgreSQL=20Global=20Development=20Group=0A=20#=20Portions=20Copyright=20= (c)=201994,=20Regents=20of=20the=20University=20of=20California=0A=20#=0A= =20#=20src/test/modules/oauth_validator/Makefile=0Adiff=20--git=20= a/src/test/modules/oauth_validator/README=20= b/src/test/modules/oauth_validator/README=0Aindex=20= 138a8104622..54eac5b117e=20100644=0A---=20= a/src/test/modules/oauth_validator/README=0A+++=20= b/src/test/modules/oauth_validator/README=0A@@=20-8,6=20+8,6=20@@=20by=20= t/OAuth/Server.pm=20and=20t/oauth_server.py,=20to=20run=20the=20libpq=20= Device=0A=20Authorization=20flow.=20The=20tests=20in=20t/002_client=20= exercise=20custom=20OAuth=20flows=20and=0A=20don't=20need=20an=20= authorization=20server.=0A=20=0A-Tests=20in=20this=20folder=20generally=20= require=20'oauth'=20to=20be=20present=20in=20PG_TEST_EXTRA,=0A-since=20= localhost=20HTTP=20servers=20will=20be=20started.=20A=20Python=20= installation=20is=20required=0A-to=20run=20the=20mock=20authorization=20= server.=0A+Tests=20in=20this=20folder=20require=20'oauth'=20to=20be=20= present=20in=20PG_TEST_EXTRA,=20since=0A+HTTPS=20servers=20listening=20= on=20localhost=20with=20TCP/IP=20sockets=20will=20be=20started.=20A=0A= +Python=20installation=20is=20required=20to=20run=20the=20mock=20= authorization=20server.=0Adiff=20--git=20= a/src/test/modules/oauth_validator/fail_validator.c=20= b/src/test/modules/oauth_validator/fail_validator.c=0Aindex=20= 7b1e69518d9..a4c7a4451d3=20100644=0A---=20= a/src/test/modules/oauth_validator/fail_validator.c=0A+++=20= b/src/test/modules/oauth_validator/fail_validator.c=0A@@=20-1,10=20+1,10=20= @@=0A=20= /*------------------------------------------------------------------------= -=0A=20=20*=0A=20=20*=20fail_validator.c=0A-=20*=09=20=20Test=20module=20= for=20serverside=20OAuth=20token=20validation=20callbacks,=20which=20= always=0A-=20*=09=20=20fails=0A+=20*=09=20=20Test=20module=20for=20= serverside=20OAuth=20token=20validation=20callbacks,=20which=20is=0A+=20= *=09=20=20guaranteed=20to=20always=20fail=20in=20the=20validation=20= callback=0A=20=20*=0A-=20*=20Portions=20Copyright=20(c)=201996-2024,=20= PostgreSQL=20Global=20Development=20Group=0A+=20*=20Portions=20Copyright=20= (c)=201996-2025,=20PostgreSQL=20Global=20Development=20Group=0A=20=20*=20= Portions=20Copyright=20(c)=201994,=20Regents=20of=20the=20University=20= of=20California=0A=20=20*=0A=20=20*=20= src/test/modules/oauth_validator/fail_validator.c=0Adiff=20--git=20= a/src/test/modules/oauth_validator/magic_validator.c=20= b/src/test/modules/oauth_validator/magic_validator.c=0Anew=20file=20mode=20= 100644=0Aindex=2000000000000..5ce68cdf405=0A---=20/dev/null=0A+++=20= b/src/test/modules/oauth_validator/magic_validator.c=0A@@=20-0,0=20+1,48=20= @@=0A= +/*-----------------------------------------------------------------------= --=0A+=20*=0A+=20*=20magic_validator.c=0A+=20*=09=20=20Test=20module=20= for=20serverside=20OAuth=20token=20validation=20callbacks,=20which=0A+=20= *=09=20=20should=20fail=20due=20to=20using=20the=20wrong=20= PG_OAUTH_VALIDATOR_MAGIC=20marker=0A+=20*=09=20=20and=20thus=20the=20= wrong=20ABI=20version=0A+=20*=0A+=20*=20Portions=20Copyright=20(c)=20= 1996-2025,=20PostgreSQL=20Global=20Development=20Group=0A+=20*=20= Portions=20Copyright=20(c)=201994,=20Regents=20of=20the=20University=20= of=20California=0A+=20*=0A+=20*=20= src/test/modules/oauth_validator/fail_validator.c=0A+=20*=0A+=20= *-------------------------------------------------------------------------= =0A+=20*/=0A+=0A+#include=20"postgres.h"=0A+=0A+#include=20"fmgr.h"=0A= +#include=20"libpq/oauth.h"=0A+=0A+PG_MODULE_MAGIC;=0A+=0A+static=20bool=20= validate_token(const=20ValidatorModuleState=20*state,=0A+=09=09=09=09=09=09= =20=20=20const=20char=20*token,=0A+=09=09=09=09=09=09=20=20=20const=20= char=20*role,=0A+=09=09=09=09=09=09=20=20=20ValidatorModuleResult=20= *result);=0A+=0A+/*=20Callback=20implementations=20(we=20only=20need=20= the=20main=20one)=20*/=0A+static=20const=20OAuthValidatorCallbacks=20= validator_callbacks=20=3D=20{=0A+=090xdeadbeef,=0A+=0A+=09.validate_cb=20= =3D=20validate_token,=0A+};=0A+=0A+const=20OAuthValidatorCallbacks=20*=0A= +_PG_oauth_validator_module_init(void)=0A+{=0A+=09return=20= &validator_callbacks;=0A+}=0A+=0A+static=20bool=0A+validate_token(const=20= ValidatorModuleState=20*state,=0A+=09=09=09=20=20=20const=20char=20= *token,=20const=20char=20*role,=0A+=09=09=09=20=20=20= ValidatorModuleResult=20*res)=0A+{=0A+=09elog(FATAL,=20"magic_validator:=20= this=20should=20be=20unreachable");=0A+=09pg_unreachable();=0A+}=0Adiff=20= --git=20a/src/test/modules/oauth_validator/meson.build=20= b/src/test/modules/oauth_validator/meson.build=0Aindex=20= 4b78c90557c..36d1b26369f=20100644=0A---=20= a/src/test/modules/oauth_validator/meson.build=0A+++=20= b/src/test/modules/oauth_validator/meson.build=0A@@=20-1,4=20+1,4=20@@=0A= -#=20Copyright=20(c)=202024,=20PostgreSQL=20Global=20Development=20Group=0A= +#=20Copyright=20(c)=202025,=20PostgreSQL=20Global=20Development=20Group=0A= =20=0A=20validator_sources=20=3D=20files(=0A=20=20=20'validator.c',=0A@@=20= -32,6=20+32,22=20@@=20fail_validator=20=3D=20= shared_module('fail_validator',=0A=20)=0A=20test_install_libs=20+=3D=20= fail_validator=0A=20=0A+magic_validator_sources=20=3D=20files(=0A+=20=20= 'magic_validator.c',=0A+)=0A+=0A+if=20host_system=20=3D=3D=20'windows'=0A= +=20=20magic_validator_sources=20+=3D=20rc_lib_gen.process(win32ver_rc,=20= extra_args:=20[=0A+=20=20=20=20'--NAME',=20'magic_validator',=0A+=20=20=20= =20'--FILEDESC',=20'magic_validator=20-=20ABI=20incompatible=20OAuth=20= validator=20module',])=0A+endif=0A+=0A+magic_validator=20=3D=20= shared_module('magic_validator',=0A+=20=20magic_validator_sources,=0A+=20= =20kwargs:=20pg_test_mod_args,=0A+)=0A+test_install_libs=20+=3D=20= magic_validator=0A+=0A=20oauth_hook_client_sources=20=3D=20files(=0A=20=20= =20'oauth_hook_client.c',=0A=20)=0Adiff=20--git=20= a/src/test/modules/oauth_validator/oauth_hook_client.c=20= b/src/test/modules/oauth_validator/oauth_hook_client.c=0Aindex=20= fc003030ff8..9f553792c05=20100644=0A---=20= a/src/test/modules/oauth_validator/oauth_hook_client.c=0A+++=20= b/src/test/modules/oauth_validator/oauth_hook_client.c=0A@@=20-4,7=20= +4,7=20@@=0A=20=20*=09=09Test=20driver=20for=20t/002_client.pl,=20which=20= verifies=20OAuth=20hook=0A=20=20*=09=09functionality=20in=20libpq.=0A=20=20= *=0A-=20*=20Portions=20Copyright=20(c)=201996-2024,=20PostgreSQL=20= Global=20Development=20Group=0A+=20*=20Portions=20Copyright=20(c)=20= 1996-2025,=20PostgreSQL=20Global=20Development=20Group=0A=20=20*=20= Portions=20Copyright=20(c)=201994,=20Regents=20of=20the=20University=20= of=20California=0A=20=20*=0A=20=20*=0Adiff=20--git=20= a/src/test/modules/oauth_validator/t/001_server.pl=20= b/src/test/modules/oauth_validator/t/001_server.pl=0Aindex=20= d2dda62a2d4..dada89e95cc=20100644=0A---=20= a/src/test/modules/oauth_validator/t/001_server.pl=0A+++=20= b/src/test/modules/oauth_validator/t/001_server.pl=0A@@=20-3,7=20+3,7=20= @@=0A=20#=20Tests=20the=20libpq=20builtin=20OAuth=20flow,=20as=20well=20= as=20server-side=20HBA=20and=20validator=0A=20#=20setup.=0A=20#=0A-#=20= Copyright=20(c)=202021-2024,=20PostgreSQL=20Global=20Development=20Group=0A= +#=20Copyright=20(c)=202021-2025,=20PostgreSQL=20Global=20Development=20= Group=0A=20#=0A=20=0A=20use=20strict;=0A@@=20-14,24=20+14,23=20@@=20use=20= MIME::Base64=20qw(encode_base64);=0A=20use=20PostgreSQL::Test::Cluster;=0A= =20use=20PostgreSQL::Test::Utils;=0A=20use=20Test::More;=0A-use=20= Config;=0A=20=0A=20use=20FindBin;=0A=20use=20lib=20$FindBin::RealBin;=0A=20= =0A=20use=20OAuth::Server;=0A=20=0A-if=20($Config{osname}=20eq=20= 'MSWin32')=0A-{=0A-=09plan=20skip_all=20=3D>=20'OAuth=20server-side=20= tests=20are=20not=20supported=20on=20Windows';=0A-}=0A-=0A=20if=20= (!$ENV{PG_TEST_EXTRA}=20||=20$ENV{PG_TEST_EXTRA}=20!~=20/\boauth\b/)=0A=20= {=0A=20=09plan=20skip_all=20=3D>=0A=20=09=20=20'Potentially=20unsafe=20= test=20oauth=20not=20enabled=20in=20PG_TEST_EXTRA';=0A=20}=0A=20=0A+if=20= ($windows_os)=0A+{=0A+=09plan=20skip_all=20=3D>=20'OAuth=20server-side=20= tests=20are=20not=20supported=20on=20Windows';=0A+}=0A+=0A=20if=20= ($ENV{with_libcurl}=20ne=20'yes')=0A=20{=0A=20=09plan=20skip_all=20=3D>=20= 'client-side=20OAuth=20not=20supported=20by=20this=20build';=0A@@=20= -570,6=20+569,24=20@@=20$node->connect_fails(=0A=20=09"fail_validator=20= is=20used=20for=20$user",=0A=20=09expected_stderr=20=3D>=20= qr/FATAL:\s+fail_validator:=20sentinel=20error/);=0A=20=0A+#=0A+#=20Test=20= ABI=20compatability=20magic=20marker=0A+#=0A= +$node->append_conf('postgresql.conf',=0A+=09"oauth_validator_libraries=20= =3D=20'magic_validator'\n");=0A+unlink($node->data_dir=20.=20= '/pg_hba.conf');=0A+$node->append_conf(=0A+=09'pg_hba.conf',=20qq{=0A= +local=20all=20test=20=20=20=20oauth=20validator=3Dmagic_validator=20=20=20= =20=20=20issuer=3D"$issuer"=20=20=20=20=20=20=20=20=20=20=20= scope=3D"openid=20postgres"=0A+});=0A+$node->restart;=0A+=0A+$log_start=20= =3D=20$node->wait_for_log(qr/ready=20to=20accept=20connections/,=20= $log_start);=0A+=0A+$node->connect_fails(=0A+=09"user=3Dtest=20= dbname=3Dpostgres=20= oauth_issuer=3D$issuer/.well-known/oauth-authorization-server/alternate=20= oauth_client_id=3Df02c6361-0636",=0A+=09"magic_validator=20is=20used=20= for=20$user",=0A+=09expected_stderr=20=3D>=20qr/FATAL:\s+OAuth=20= validator=20module=20"magic_validator":=20magic=20number=20mismatch/);=0A= =20$node->stop;=0A=20=0A=20done_testing();=0Adiff=20--git=20= a/src/test/modules/oauth_validator/t/002_client.pl=20= b/src/test/modules/oauth_validator/t/002_client.pl=0Aindex=20= 95cccf90dd8..ab83258d736=20100644=0A---=20= a/src/test/modules/oauth_validator/t/002_client.pl=0A+++=20= b/src/test/modules/oauth_validator/t/002_client.pl=0A@@=20-2,7=20+2,7=20= @@=0A=20#=20Exercises=20the=20API=20for=20custom=20OAuth=20client=20= flows,=20using=20the=20oauth_hook_client=0A=20#=20test=20driver.=0A=20#=0A= -#=20Copyright=20(c)=202021-2024,=20PostgreSQL=20Global=20Development=20= Group=0A+#=20Copyright=20(c)=202021-2025,=20PostgreSQL=20Global=20= Development=20Group=0A=20#=0A=20=0A=20use=20strict;=0Adiff=20--git=20= a/src/test/modules/oauth_validator/t/OAuth/Server.pm=20= b/src/test/modules/oauth_validator/t/OAuth/Server.pm=0Aindex=20= f0f23d1d1a8..655b2870b0b=20100644=0A---=20= a/src/test/modules/oauth_validator/t/OAuth/Server.pm=0A+++=20= b/src/test/modules/oauth_validator/t/OAuth/Server.pm=0A@@=20-1,5=20+1,5=20= @@=0A=20=0A-#=20Copyright=20(c)=202024,=20PostgreSQL=20Global=20= Development=20Group=0A+#=20Copyright=20(c)=202025,=20PostgreSQL=20Global=20= Development=20Group=0A=20=0A=20=3Dpod=0A=20=0A@@=20-46,7=20+46,7=20@@=20= use=20Test::More;=0A=20=0A=20=3Dover=0A=20=0A-=3Ditem=20= SSL::Server->new()=0A+=3Ditem=20OAuth::Server->new()=0A=20=0A=20Create=20= a=20new=20OAuth=20Server=20object.=0A=20=0Adiff=20--git=20= a/src/test/modules/oauth_validator/validator.c=20= b/src/test/modules/oauth_validator/validator.c=0Aindex=20= e218f5c8902..b2e5d182e1b=20100644=0A---=20= a/src/test/modules/oauth_validator/validator.c=0A+++=20= b/src/test/modules/oauth_validator/validator.c=0A@@=20-3,7=20+3,7=20@@=0A= =20=20*=20validator.c=0A=20=20*=09=20=20Test=20module=20for=20serverside=20= OAuth=20token=20validation=20callbacks=0A=20=20*=0A-=20*=20Portions=20= Copyright=20(c)=201996-2024,=20PostgreSQL=20Global=20Development=20Group=0A= +=20*=20Portions=20Copyright=20(c)=201996-2025,=20PostgreSQL=20Global=20= Development=20Group=0A=20=20*=20Portions=20Copyright=20(c)=201994,=20= Regents=20of=20the=20University=20of=20California=0A=20=20*=0A=20=20*=20= src/test/modules/oauth_validator/validator.c=0A--=20=0A2.39.3=20(Apple=20= Git-146)=0A=0A= --Apple-Mail=_43C3B3B1-87EA-4831-9D4F-5C67D4DD918F--