diff --git a/_data/authors.yaml b/_data/authors.yaml index 360c474facb..ae826ec0d54 100644 --- a/_data/authors.yaml +++ b/_data/authors.yaml @@ -520,4 +520,11 @@ andreatp: emailhash: "6250a4b95ad7ddcf2ba7c3f84ba47995" job_title: "Principal Software Engineer" twitter: "and_prf" - bio: "Principal Software Engineer at Red Hat, working in Middleware Application Services on the Apicurio projects. Involved in the development of Microsoft's Kiota and Dylibso's Chicory." \ No newline at end of file + bio: "Principal Software Engineer at Red Hat, working in Middleware Application Services on the Apicurio projects. Involved in the development of Microsoft's Kiota and Dylibso's Chicory." +sberyozkin: + name: "Sergey Beryozkin" + email: "sberyozkin@gmail.com" + emailhash: "7941c5788c2e02a4f7f51409afca3090" + job_title: "Software Engineer" + twitter: "sberyozkin" + bio: "Software Engineer, working at Red Hat on Quarkus Security." diff --git a/_posts/2024-04-01-oidc-proxy.adoc b/_posts/2024-04-01-oidc-proxy.adoc new file mode 100644 index 00000000000..0fba6a73fd8 --- /dev/null +++ b/_posts/2024-04-01-oidc-proxy.adoc @@ -0,0 +1,456 @@ +--- +layout: post +title: 'Use OIDC Proxy to integrate OIDC service endpoints with custom GPT' +date: 2024-03-08 +tags: extension oidc oidc-proxy security chatgpt gpt development-tips +synopsis: 'Explain how OIDC Proxy can help to integrate OIDC service endpoints with custom GPT' +author: sberyozkin +--- +:imagesdir: /assets/images/posts/oidc-proxy + +== Introduction + +https://github.com/quarkiverse/quarkus-oidc-proxy[Quarkus OIDC Proxy] is a new https://github.com/quarkiverse[Quarkiverse] extension which can help to integrate https://quarkus.io/guides/security-oidc-bearer-token-authentication[OIDC service endpoints] with external Single-page applications (SPA). + +SPA uses the OIDC authorization code flow, without relying on Quarkus, to authenticate the current user, and accesses the Quarkus OIDC service endpoint with the access token on behalf of the authenticated user. Here is a simple diagram showing how this process works, copied to this post from the https://quarkus.io/guides/security-oidc-bearer-token-authentication[OIDC Bearer token guide] for your convenience: + +image::security-bearer-token-spa.png[SPA and Quarkus Service,align="center"] + +You can see that the OIDC provider is used to authenticate the current user to SPA. SPA acquires ID, access and, possibly, refresh tokens as part of the authorization code flow and uses the access token to access the Quarkus OIDC service endpoint. + +SPA which integrates with the OIDC provider directly must know the provider connection details, including the registered OIDC application's client id, and other OIDC specific details required to complete the authorization code flow successfully. You must also allow an SPA specific callback URL in your registered OIDC application which may not always be acceptable. + +https://github.com/quarkiverse/quarkus-oidc-proxy[Quarkus OIDC Proxy] proxies the OIDC provider in the diagram above. It interposes over Quarkus OIDC service endpoints and delegates to the real OIDC provider to support an authorization code flow. It can help to integrate OIDC service endpoints with SPA without having to expose the internal OIDC connection details to this SPA. It relies on Quarkus OIDC to let SPA authenticate its users to OIDC and OAuth2 providers which may be technically challenging to support directly at the SPA level. + +Another use case for OIDC Proxy is to have several https://quarkus.io/guides/security-oidc-code-flow-authentication[Quarkus OIDC web-app] endpoints to authenticate users using the same OIDC proxy configuration before accessing the OIDC service endpoint. + +So how does OIDC proxy actually work ? Sure, we will look at it shortly, but first, let's talk about custom GPT actions. + +[[gpt_actions]] +== Custom GPT Actions + +https://chat.openai.com[ChatGPT] has introduced https://platform.openai.com/docs/actions/introduction[Actions], which can be used to create custom GPTs. For example, you can create a custom GPT which can enhance your ChatGPT conversation experience by connecting it to your API endpoints. + +The key challenge is how a custom GPT can be https://platform.openai.com/docs/actions/authentication[authenticated] to be allowed to access the API. The https://platform.openai.com/docs/actions/authentication/oauth[OAuth] option is the best option when you need a user-specific permission to access the API, and this is what https://github.com/quarkiverse/quarkus-oidc-proxy[Quarkus OIDC Proxy] will help you to support without exposing all the OIDC/OAuth2 connection details to the GPT. + +Please be aware that currently, custom GPT actions can not be created with a free ChatGPT subscription, but only starting from the ChatGPT Plus subscription. + +[[fitness_adviser]] +== Quarkus Fitness Adviser + +In this section we will create `Quarkus Fitness Adviser`, a custom GPT which analyzes activities recorded in Strava and other social providers which track physical exercise. + +We will do it by registering a https://www.strava.com/[Strava] API Application, creating a https://quarkus.io/guides/security-openid-connect-providers#strava[Strava OAuth2] service endpoint, proxying it with https://github.com/quarkiverse/quarkus-oidc-proxy[OIDC Proxy], providing an HTTPS tunnel with <> and finally, creating a custom GPT which uses https://github.com/quarkiverse/quarkus-oidc-proxy[OIDC Proxy] to autenticate the GPT users to Strava and use access tokens to access the Quarkus Strava OIDC service endpoint to analyze the recorded activities. + +[[strava_application_registration]] +=== Strava Application Registration + +We will start by registering a new `Quarkus Fitness Adviser` application in Strava: + +image::strava-application-registration.png[Strava Application Registration,align="center"] + +Note that the `Authorization Callback Domain` points to your free <> (or in production, the real) domain representing the domain where OIDC Proxy is available, likely to be the same domain where your Quarkus micro-services are hosted as well. It is an important feature of Quarkus OIDC Proxy as it lets OIDC provider administrators to point to the trusted domain as opposed to a 3rd party domain. + +Also note that the fact that only a domain is accepted as a callback option is specific to the https://www.strava.com/[Strava] application registration process. Allowing only specific absolute callback URLs is recommended in general and the Quarkus https://quarkus.io/guides/security-openid-connect-providers#strava[Strava OAuth2] integration enforces that only a single callback URL is accepted from your Strava OAuth2 application. + +After completing the application registration, note the generated client id and secret. + +[[strava_service]] +=== Quarkus Strava Service + +Quarkus OIDC supports https://quarkus.io/guides/security-openid-connect-providers#strava[Strava OAuth2 provider] by encapsulating Strava OAuth2 specific details in a single configuration line, `quarkus.oidc.provider=strava`. + +Strava provider is mostly OAuth2 compliant but it uses HTTP query parameters to complete the authorization code flow POST token request, when using the form parameters is a usual option. It also uses a comma `,` separator when multiple scopes are requested during the initial redirect to Strava, with a space ' ' being a typical separator character. + +Quarkus OIDC proxy can handle it because it can use the Quarkus OIDC knowledge about these details. An SPA such as a custom GPT does not support these options with its OAuth authentication option. + +Add the following Maven dependencies to your project: + +[source,xml] +---- + + io.quarkus + quarkus-oidc + + + io.quarkus + quarkus-oidc-token-propagation-reactive + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-smallrye-openapi + +---- + +It enables Quarkus JAX-RS, OIDC and OpenAPI capabilities. Note that Quarkus version must be at least `3.9.0.CR2`. + +Next we create the OIDC configuration: + +[source,properties] +---- +quarkus.oidc.provider=strava +quarkus.oidc.application-type=service + +quarkus.oidc.client-id=${strava-client-id} +quarkus.oidc.credentials.secret=${strava-client-secret} +quarkus.oidc.authentication.extra-params.scope=profile:read_all,activity:read_all +---- + +By default, `quarkus.oidc.provider=strava` enables a Quarkus OIDC `web-app` application type which can support an authorization code flow. But this endpoint has to act as a Quarkus OIDC `service` which accepts the bearer access tokens from the GPT, so we set the application type to `service` with `quarkus.oidc.application-type=service`. It is OIDC Proxy which will manage the authorization code flow instead. + +Note how the extra https://developers.strava.com/docs/reference/[Strava API] scopes are added to the scopes which are already enabled by `quarkus.oidc.provider=strava`, instead of overriding them. See https://quarkus.io/guides/security-openid-connect-providers#provider-scope[Provider scopes] for more information. + +The client id, secret and the extra scopes are not really required by the OIDC service endpoint. These properties are set to support OIDC Proxy which needs to know how to correctly handle the OIDC authorization code flow requests from the external SPA. + +We also add the following properties: + +[source,properties] +---- +quarkus.rest-client.strava-client.url=https://www.strava.com/api/v3 + +quarkus.smallrye-openapi.operation-id-strategy=method +quarkus.smallrye-openapi.auto-add-security=false +quarkus.smallrye-openapi.servers=https://.app +---- + +First, we configure a Strava REST client to point to the base Strava API endpoint. And we tune a little bit the way an https://quarkus.io/guides/openapi-swaggerui[OpenAPI document is generated by Quarkus] to have it acceptable by a custom GPT configuration process. + +Here is the REST client which https://quarkus.io/guides/security-openid-connect-providers#access-provider-services-with-token-propagation[propagates] Strava access tokens to access the user-specific Strava data: + +[source,java] +---- +package org.acme.security.openid.connect.plugin; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.token.propagation.AccessToken; +import io.smallrye.mutiny.Uni; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@RegisterRestClient(configKey="strava-client") +@AccessToken +@Path("/") +public interface StravaClient { + + @GET + @Path("athlete/activities") + @Produces(MediaType.APPLICATION_JSON) + Uni athleteActivities(); + + @GET + @Path("activities/{id}") + @Produces(MediaType.APPLICATION_JSON) + Uni athleteActivity(@PathParam("id") long activityId); + + @GET + @Path("athletes/{id}/stats") + @Produces(MediaType.APPLICATION_JSON) + Uni athleteStats(@PathParam("id") long athleteId); + + // Etc for other Strava API +} +---- + +Here is the service endpoint which accepts the access tokens from a custom GPT and uses the REST client to forward them to Strava: + +[source,java] +---- +package org.acme.security.openid.connect.plugin; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.quarkus.logging.Log; +import io.quarkus.oidc.UserInfo; +import io.quarkus.security.Authenticated; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; + +@Path("/athlete") +@Authenticated +public class FitnessAdviserService { + + @Inject + UserInfo athlete; + + @Inject + @RestClient + StravaClient stravaClient; + + @GET + @Produces("application/json") + public Uni athlete() { + Log.info("Fitness adviser: athlete"); + return Uni.createFrom().item(athlete.getJsonObject().toString()); + } + + @GET + @Produces("application/json") + @Path("/activities") + public Uni activities() { + Log.info("Fitness adviser: activities"); + return stravaClient.athleteActivities(); + } + + @GET + @Produces("application/json") + @Path("/activity/{id}") + public Uni activity(@PathParam("id") long activityId) { + Log.infof("Fitness adviser: activity %d", activityId); + return stravaClient.athleteActivity(activityId); + } + + @GET + @Produces("application/json") + @Path("/stats") + public Uni stats() { + Log.info("Fitness adviser: stats"); + return stravaClient.athleteStats(athlete.getLong("id")); + } + + // Etc for other Strava API +} +---- + +Note, in order to accept binary Strava access tokens, this endpoint is verifying them indirectly by requesting `UserInfo` from Strava, during the token authentication process, which is enabled by the `quarkus.oidc.provider=strava` declaration. +In this case `UserInfo` represents a Strava athlete profile which is already available to the endpoint by the time it makes an outbound REST client call. For example, the `FitnessAdviserService` endpoint passes a `UserInfo` athlete `id` attribute to `StravaClient` to request the current authenticated athlete's stats. + +If it were an access token issued by a provider such as Keycloak or Auth0 then it would be verified locally with the Keycloak or Auth0 public verification keys and https://quarkus.io/guides/security-oidc-bearer-token-authentication#accessing-jwt-claims[injected directly as JsonWebToken]. + +[[oidc_proxy]] +=== OIDC Proxy + +Now that we have created the OIDC Strava service endpoint, it is time to make it accessible to the external SPA using OIDC Proxy and an authorization code flow authentication process. + +All you have to do is to add: + +[source,xml] +---- + + io.quarkiverse.oidc-proxy + quarkus-oidc-proxy + 0.1.0 + +---- + +It will enable OIDC `/q/oidc/authorize` for accepting custom GPT authentication redirects, `/q/oidc/token` for exchanging an authorization code for tokens, +and other OIDC endpoints at the `/q/oidc` root path. + +Let's update the application configuration: + +[source,properties] +---- +quarkus.oidc.authentication.redirect-path=/callback <1> +quarkus.oidc-proxy.external-redirect-uri=https://chat.openai.com/aip/g-2faf163d359505ecb63596f17baa3dfe53ea3cb9/oauth/callback <2> +quarkus.oidc.authentication.force-redirect-https-scheme=true <3> +quarkus.oidc-proxy.root-path=/oidc <4> +quarkus.oidc-proxy.external-client-id=external-client-id <5> +quarkus.oidc-proxy.external-client-secret=external-client-secret <5> +---- +<1> Request OIDC Proxy to create an endpoint which will support redirects from the real OIDC provider. As explained in the <> section, it can be useful to register the known, trusted domain URL in the OIDC provider's dashboard. This property is already set to `/strava` with the Strava provider by default in order to restrict the possible callback URLs as explained in the <> section, this example shows how it can be customized. You do not have to use `quarkus.oidc.authentication.redirect-path` with other providers, but please be aware of this property. +<2> The external callback URL where OIDC Proxy will redirect the user to after accepting the `quarkus.oidc.authentication.redirect-path` callback. +<3> <> will terminate the HTTPS protocol before calling an `HTTP` based endpoint, so the original `HTTPS` scheme must be used for building an external redirect URL. +<4> OIDC Proxy's default root path is `/q/oidc`, set it to `/oidc` to demonstrate how it can be changed. +<5> Set the external client id and secret that will be used during the integration with the 3rd party SPA. Use these properties if you do not want to expose +the real client id and secret to the SPA. + +Build the application now and start it in the JVM mode: + +[source,bash] +---- +mvn clean install +java target/quarkus-app/quarkus-run.jar +---- + +If you would like to test in it devmode then, in order to allow the redirects from the external SPA to the OIDC Proxy authorization endpoint, you have to disable the DevUI CORS control: + +[source,properties] +---- +%dev.quarkus.dev-ui.cors.enabled=false +---- + +[[ngrok]] +=== NGrok + +3rd party SPA will most likely require that the OIDC provider endpoints are HTTPS-based, therefore, to make OIDC Proxy endpoints use the HTTPS scheme on the localhost, using https://ngrok.com/[NGrok] is the simplest way to do it. + +Note that: + +[source,bash] +---- +ngrok http --domain 8080 +---- + +does not prevent the NGrok warning that the website is served for free from NGrok, which confuses the custom GPT's OAuth authorization code flow support. +In this case you should enable an HTTP tunnel as described in this https://stackoverflow.com/questions/73017353/how-to-bypass-ngrok-browser-warning[Stack Overflow post], for example: + +[source,bash] +---- +ngrok tunnel --label edge= http://localhost:8080 +---- + +=== Create custom GPT + +As noted in the <> section, creating custom GPTs is only possible starting from the ChatGPT Plus subscription. Please see the <> section below for other suggestions to experiment with OIDC Proxy. + +Login to your ChatGPT account, and choose `Create` in `My GPTs`: + +image::create-custom-gpt.png[Create custom GPT,align="center"] + +Name it as `Quarkus Fitness Adviser` and provide its description: + +image::custom-gpt-description.png[Custom GPT description,align="center"] + +Next, choose an `OAuth` authentication option: + +image::custom-gpt-select-oauth.png[Custom GPT OAuth option,align="center"] + +and set the OAuth2 authorize and token endpoint addresses, keeping in mind your free <> domain name and that you have set the OIDC Proxy root address to `/oidc` in the <> section: + +image::custom-gpt-configure-oauth.png[custom GPT OAuth configuration,align="center"] + +Set the client id and secret to the external client id and external client secret properties which you configured in the <> section. + +Now you can see that this custom GPT's OAuth setup has been completed without sharing a single detail related to the Strava provider configuration in the Quarkus OIDC service endpoint. +You also do not need to set the scopes, OIDC Proxy knows about them from the Quarkus OIDC endpoint configuration. + +Next, import an OpenAPI schema by choosing an `Import from URL` option and entering `http:///q/openapi`: + +image::custom-gpt-import-openapi.png[Custom GPT Import OpenAPI,align="center"] + +At this point you are ready to save this GPT and start using it. + +Note this GPT's callback, this is the external callback URI value you configured in the <> section: + +image::custom-gpt-callback.png[Custom GPT callback,align="center"] + +You have to decide if you would like to share this GPT. Most likely, after testing it, you will prefer to share it with your team to test it, and eventually, with your customers. + +In this case, the first thing you have to do is to ask ChatGPT for a typical privacy policy text, if you do not already have it, and after modifying it as necessary, save it, for example, in a `privacy.txt` document in the `src/main/resources/META-INF/resources/` of your <> application and link to it like this: + +image::custom-gpt-privacy-policy.png[Custom GPT Privacy policy,align="center"] + +Finally, publish it using the `Anyone with a link` option: + +image::custom-gpt-share-link.png[Custom GPT Share Link,align="center"] + +`Quarkus Fitness Adviser` is now ready: + +image::custom-gpt-is-ready.png[Custom GPT is ready,align="center"] + +[[use_custom_gpt]] +=== Use custom GPT + +Let's start with asking `Quarkus Fitness Adviser` to check the athlete profile: + +image::custom-gpt-sign-in.png[Custom GPT Sign In,align="center"] + +When you ask the GPT the first question, it will attempt to sign you in using the OAuth authentication option. Select the `Sign in` option and you will be redirected to OIDC Proxy which will in turn redirect to Strava to authenticate: + +image::oidc-proxy-strava-login.png[Strava Login,align="center"] + +Enter your Strava name and password and continue. You will be asked to authenticate again only when the access token acquired with the authorization code flow has expired. + +After the successful authentication you will be asked to authorize the `Quarkus Fitness Adviser` applicaton which you registered in the <> section: + +image::strava-application-authorization.png[Strava Authorization,align="center"] + +The https://developers.strava.com/docs/authentication/#detailsaboutrequestingaccess[Strava API scopes] which have been configured for the <> affect what you will be asked to authorize. + +You will now be redirected to the custom GPT with the authorization code which will be exchanged for the access and refresh tokens using OIDC Proxy. +The GPT will now want to talk to the Quarkus API and ask you to approve it: + +image::custom-gpt-approve-action.png[Custom GPT Approve Action,align="center"] + +Approve it and `Quarkus Fitness Adviser` will provide the first answer: + +image::custom-gpt-profile-overview.png[Custom GPT Profile Overview,align="center"] + +It also provides information about your bike, running shoes, etc and gives some initial recommendations, finishing with a question: + +image::custom-gpt-specific-advice.png[Custom GPT Profile Advice,align="center"] + +We can now ask for some advice on balancing cycling and swimming, about running, etc. +Next, let's ask about the most challenging activity: + +image::custom-gpt-challenging-activity.png[Custom GPT Challenging Activity,align="center"] + +and the latest one: + +image::custom-gpt-latest-activity.png[Custom GPT Latest Activity,align="center"] + +Ask it to be more specific about the latest activity and provide some advice: + +image::custom-gpt-activity-recommendation.png[Custom GPT Activity Recommendation,align="center"] + +An advice to have a good rest is always sound: + +image::custom-gpt-recommends-to-rest.png[Custom GPT Rest Advice,align="center"] + +Finally, let's ask it to check the profile again and provide more recommendations. `Quarkus Fitness Adviser` is happy to help and provides, in my case, eight personalized recommendations, I will only show the start of the response: + +image::custom-gpt-profile-recommendations.png[Custom GPT More Profile Recommendations,align="center"] + +and the end of it: + +image::custom-gpt-enjoy-the-ride.png[Custom GPT Enjoy the Ride,align="center"] + +We will return to this advice later in this post. + +Let's finish by saying `Thank you`: + +image::custom-gpt-final-message.png[Custom GPT Final Message,align="center"] + +[[next-steps]] +== Next Steps + +So far, `Quarkus Fitness Adviser` has helped to analyze the authenticated athlete's profile and activities. +Please experiment further by creating a more advanced version of `Quarkus Fitness Adviser` by checking the routes, zones, and other fitness data supported by the https://developers.strava.com/docs/reference/[Strava API]. + +Create a new custom GPT with the help of https://quarkus.io/guides/security-openid-connect-providers[any other well-known social provider supported in Quarkus]. + +Also note, your Quarkus OIDC service endpoint does not have to propagate the access token. For example, if you use Keycloak or Auth0, then the access tokens in JWT formats issued by these OIDC compliant providers can be verified by Quarkus OIDC to provide a role-based or permission-based access control for custom GPT's requests, with the service endpoint returning data from the database, etc. + +You are also encouraged to look closely at the https://github.com/quarkiverse/quarkus-langchain4j[Quarkus LangChain4j] project which provides a top class integration between Quarkus and the https://github.com/langchain4j/langchain4j[LangChain4j] library. + +How about creating a custom GPT which will use OIDC Proxy to authenticate custom GPT users to Keycloak or Auth0 or Azure and access Quarkus OIDC service endpoint powered by https://github.com/quarkiverse/quarkus-langchain4j[Quarkus LangChain4j] ? Give it a try please ! + +What if you do not have a ChatGPT Plus subscription ? + +Not a problem, OIDC Proxy will work with any SPA which implements an authorization code flow and prefers to have an OIDC provider neutral integration, please test OIDC Proxy with such SPAs. + +Alternatively, experiment with configuring Quarkus OIDC `web-app` applications using OIDC Proxy to authenticate users before calling OIDC service endpoints. For example, imagine three different Quarkus OIDC `web-app` applications using the same Keycloak realm to authenticate the users with an authorization code flow and propagating the access tokens to the same OIDC `service` application. Now, instead of setting the Keycloak specific details in all of the OIDC `web-app` applications, you can try to add OIDC Proxy to the OIDC Service endpoint and configure the OIDC `web-app` applications to use OIDC Proxy. + +== Security Considerations + +You have already seen several OIDC Proxy security features in the <> section. + +General OIDC Proxy feature is about hiding all the real OIDC provider specific details from the SPA, including all the OAuth2 or OIDC provider specific details, as well as the extra scopes which are requested during the authentication redirect to the provider. + +OIDC Proxy allows you to set the trusted domain in the allowed callback URI which is registered in the OIDC provider and enables a callback bridge between the real OIDC provider and the external SPA. + +You can hide the real client id and client secret which OIDC Proxy must use from the external SPA. + +You can request that OIDC Proxy does not return a refresh and/or ID token from the authorization code token exchange to the SPA. + +Refresh token is the most powerful token, usually with a long life-span. If an SPA leaks it, alongside the client id and secret, the attacker can +refresh and use access tokens to access the API for a long time. Therefore, if you are concerned about SPA, such as a custom GPT, possibly leaking this information, add `quarkus.oidc-proxy-allow-refresh-token=false` to the configuration to request OIDC Proxy to remove the refresh token value from the authorization code flow response which it is about to return to the GPT. It will not block a given custom GPT from using the Quarkus API, it will only require this GPT to re-authenticate the user when the access token has expired, as opposed to refreshing it. + +ID token contains information about the currently authenticated user. If you know that the SPA does not need an ID token, such as a custom GPT which only works with the access and refresh tokens, then it is recommended to block returning it with `quarkus.oidc-proxy-allow-id-token=false` + +== Conclusion + +In this post we have looked at how https://github.com/quarkiverse/quarkus-oidc-proxy[Quarkus OIDC Proxy] can help to integrate OIDC service endpoints with SPA without having to expose the internal OIDC connection details. We have built `Quarkus Fitness Adviser`, a https://platform.openai.com/docs/actions/introduction[custom GPT], which uses OIDC Proxy to authenticate users with https://quarkus.io/guides/security-openid-connect-providers#strava[Strava] and provides fitness advice by reading the authenticated user-specific data from the Quarkus OIDC Strava service. + +Enjoy Quarkus, and, as `Quarkus Fitness Adviser` recommends, enjoy the ride ! diff --git a/assets/images/posts/oidc-proxy/create-custom-gpt.png b/assets/images/posts/oidc-proxy/create-custom-gpt.png new file mode 100644 index 00000000000..bcd551983ff Binary files /dev/null and b/assets/images/posts/oidc-proxy/create-custom-gpt.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-activity-recommendation.png b/assets/images/posts/oidc-proxy/custom-gpt-activity-recommendation.png new file mode 100644 index 00000000000..88e4de1ae4f Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-activity-recommendation.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-allow-action.png b/assets/images/posts/oidc-proxy/custom-gpt-allow-action.png new file mode 100644 index 00000000000..ad73d1b8974 Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-allow-action.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-callback.png b/assets/images/posts/oidc-proxy/custom-gpt-callback.png new file mode 100644 index 00000000000..a8f86ddcfc5 Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-callback.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-challenging-activity.png b/assets/images/posts/oidc-proxy/custom-gpt-challenging-activity.png new file mode 100644 index 00000000000..029bc80a5ff Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-challenging-activity.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-configure-oauth.png b/assets/images/posts/oidc-proxy/custom-gpt-configure-oauth.png new file mode 100644 index 00000000000..845d2a82312 Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-configure-oauth.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-description.png b/assets/images/posts/oidc-proxy/custom-gpt-description.png new file mode 100644 index 00000000000..287ac98e37b Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-description.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-enjoy-the-ride.png b/assets/images/posts/oidc-proxy/custom-gpt-enjoy-the-ride.png new file mode 100644 index 00000000000..7b125c3aef4 Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-enjoy-the-ride.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-final-message.png b/assets/images/posts/oidc-proxy/custom-gpt-final-message.png new file mode 100644 index 00000000000..c241860fe2e Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-final-message.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-import-openapi.png b/assets/images/posts/oidc-proxy/custom-gpt-import-openapi.png new file mode 100644 index 00000000000..2f8121a6137 Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-import-openapi.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-is-ready.png b/assets/images/posts/oidc-proxy/custom-gpt-is-ready.png new file mode 100644 index 00000000000..aa968ed008f Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-is-ready.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-latest-activity.png b/assets/images/posts/oidc-proxy/custom-gpt-latest-activity.png new file mode 100644 index 00000000000..9a17eaf6adc Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-latest-activity.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-privacy-policy.png b/assets/images/posts/oidc-proxy/custom-gpt-privacy-policy.png new file mode 100644 index 00000000000..a2269024e8b Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-privacy-policy.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-profile-overview.png b/assets/images/posts/oidc-proxy/custom-gpt-profile-overview.png new file mode 100644 index 00000000000..3c8db71a333 Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-profile-overview.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-profile-recommendations.png b/assets/images/posts/oidc-proxy/custom-gpt-profile-recommendations.png new file mode 100644 index 00000000000..8b61ccef9f0 Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-profile-recommendations.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-recommends-to-rest.png b/assets/images/posts/oidc-proxy/custom-gpt-recommends-to-rest.png new file mode 100644 index 00000000000..2b9f4ac3e20 Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-recommends-to-rest.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-select-oauth.png b/assets/images/posts/oidc-proxy/custom-gpt-select-oauth.png new file mode 100644 index 00000000000..1f2229a98e5 Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-select-oauth.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-share-link.png b/assets/images/posts/oidc-proxy/custom-gpt-share-link.png new file mode 100644 index 00000000000..5db5112c302 Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-share-link.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-sign-in.png b/assets/images/posts/oidc-proxy/custom-gpt-sign-in.png new file mode 100644 index 00000000000..4bef03b5844 Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-sign-in.png differ diff --git a/assets/images/posts/oidc-proxy/custom-gpt-specific-advice.png b/assets/images/posts/oidc-proxy/custom-gpt-specific-advice.png new file mode 100644 index 00000000000..5d50474b2e9 Binary files /dev/null and b/assets/images/posts/oidc-proxy/custom-gpt-specific-advice.png differ diff --git a/assets/images/posts/oidc-proxy/oidc-proxy-strava-login.png b/assets/images/posts/oidc-proxy/oidc-proxy-strava-login.png new file mode 100644 index 00000000000..042498e0b9d Binary files /dev/null and b/assets/images/posts/oidc-proxy/oidc-proxy-strava-login.png differ diff --git a/assets/images/posts/oidc-proxy/security-bearer-token-spa.png b/assets/images/posts/oidc-proxy/security-bearer-token-spa.png new file mode 100644 index 00000000000..13271519264 Binary files /dev/null and b/assets/images/posts/oidc-proxy/security-bearer-token-spa.png differ diff --git a/assets/images/posts/oidc-proxy/strava-application-authorization.png b/assets/images/posts/oidc-proxy/strava-application-authorization.png new file mode 100644 index 00000000000..cbcdfd16216 Binary files /dev/null and b/assets/images/posts/oidc-proxy/strava-application-authorization.png differ diff --git a/assets/images/posts/oidc-proxy/strava-application-registration.png b/assets/images/posts/oidc-proxy/strava-application-registration.png new file mode 100644 index 00000000000..7e3efed0db8 Binary files /dev/null and b/assets/images/posts/oidc-proxy/strava-application-registration.png differ