{"id":4092,"date":"2025-06-28T11:23:33","date_gmt":"2025-06-28T10:23:33","guid":{"rendered":"https:\/\/discovery.cevolution.co.uk\/ciam\/?p=4092"},"modified":"2026-04-05T14:04:52","modified_gmt":"2026-04-05T13:04:52","slug":"vibe-coded-apis","status":"publish","type":"post","link":"https:\/\/discovery.cevolution.co.uk\/ciam\/2025\/06\/28\/vibe-coded-apis\/","title":{"rendered":"AI Assist with Vibe Coded Auth for Your APIs"},"content":{"rendered":"<span class=\"span-reading-time rt-reading-time\" style=\"display: block;\"><span class=\"rt-label rt-prefix\">Reading Time: <\/span> <span class=\"rt-time\"> 16<\/span> <span class=\"rt-label rt-postfix\">minutes<\/span><\/span>\n<p>With the rise of microservices, interconnected applications, and even AI, all enabled by <span class=\"popup-trigger popmake-2876\" data-popup-id=\"2876\" data-do-default=\"0\">APIs<\/span>, secure access has become an increasingly complex challenge. In a previous article, and with the help of AI itself, I discussed using something called <em><span class=\"popup-trigger popmake-3653\" data-popup-id=\"3653\" data-do-default=\"0\">Authorization Code Flow<\/span><\/em> \u2014 a key aspect of both the <span class=\"popup-trigger popmake-407\" data-popup-id=\"407\" data-do-default=\"0\">OIDC<\/span> and <span class=\"popup-trigger popmake-467\" data-popup-id=\"467\" data-do-default=\"0\">OAuth 2.0<\/span> protocols \u2014 in order to obtain an OIDC <span class=\"popup-trigger popmake-1393\" data-popup-id=\"1393\" data-do-default=\"0\">ID Token<\/span> that an application can use to verify user <a href=\"https:\/\/discovery.cevolution.co.uk\/ciam\/authenticate\/\" target=\"_blank\" rel=\"noopener\" title=\"Authenticate\">Authentication<\/a>.<\/p>\n\n\n\n<div class=\"wp-block-group is-content-justification-center is-nowrap is-layout-flex wp-container-core-group-is-layout-23441af8 wp-block-group-is-layout-flex\">\n<figure class=\"wp-block-embed is-type-wp-embed is-provider-discover-ciam wp-block-embed-discover-ciam\"><div class=\"wp-block-embed__wrapper\">\n<blockquote class=\"wp-embedded-content\" data-secret=\"MMyloaVokS\"><a href=\"https:\/\/discovery.cevolution.co.uk\/ciam\/2025\/05\/16\/vibe-coded-authn\/\">Vibe Coding Authentication via Authorization Code Flow<\/a><\/blockquote><iframe loading=\"lazy\" class=\"wp-embedded-content\" sandbox=\"allow-scripts\" security=\"restricted\" style=\"position: absolute; visibility: hidden;\" title=\"&#8220;Vibe Coding Authentication via Authorization Code Flow&#8221; &#8212; Discover CIAM\" src=\"https:\/\/discovery.cevolution.co.uk\/ciam\/2025\/05\/16\/vibe-coded-authn\/embed\/#?secret=XX3XvhJea3#?secret=MMyloaVokS\" data-secret=\"MMyloaVokS\" width=\"500\" height=\"282\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\"><\/iframe>\n<\/div><\/figure>\n<\/div>\n\n\n\n<p>I&#8217;m going to carry on in a similar vein and, with the help of AI, extend the process to help protect some APIs as part of a CIAM integration. My name&#8217;s <span class=\"popup-trigger popmake-378\" data-popup-id=\"378\" data-do-default=\"0\">Peter Fernandez<\/span>, and in this article \u2014 together with its accompanying video \u2014 I&#8217;ll be showing you how Vibe coding helped me integrate the use of OAuth 2.0 <span class=\"popup-trigger popmake-1400\" data-popup-id=\"1400\" data-do-default=\"0\">Access Tokens<\/span> to help me protect my first-party APIs in the context of my first-party Application.<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-f255541a561fb231bdc88aa87a73ae8a is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>I&#8217;ve also created a GitHub repo <a href=\"https:\/\/github.com\/PeterGFernandez\/my-nextjs-app\" target=\"_blank\" rel=\"noopener\" title=\"\">here<\/a>, where you can grab the code associated with my various Vibe coding exploits in the context of the  <code>\"my-nextjs-app\"<\/code> \u2014 as in the code I&#8217;m creating as part of this article, as well as the code created as part of the previous article, too.<\/em><\/p>\n<\/div>\n\n\n\n<figure class=\"wp-block-embed is-type-rich is-provider-embed-handler wp-block-embed-embed-handler wp-embed-aspect-16-9 wp-has-aspect-ratio\"><div class=\"wp-block-embed__wrapper\">\n<iframe loading=\"lazy\" title=\"Vibe Coding Auth for First Party APIs\" width=\"500\" height=\"281\" src=\"https:\/\/www.youtube.com\/embed\/01Iy9ZPJFnA?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe>\n<\/div><\/figure>\n\n\n<h2 class=\"wp-block-heading\" id=\"what-is-a-firstparty-api\">What is a First-Party API?<\/h2>\n\n\n<p>You may be asking yourself what I mean when I say &#8220;first-party&#8221;? A <strong>first-party API<\/strong> is essentially an API \u2014 in <span class=\"popup-trigger popmake-467\" data-popup-id=\"467\" data-do-default=\"0\">OAuth 2.0<\/span> terms, also referred to as a <em>Resource Server<\/em> \u2014 that I, as a developer, have written. <\/p>\n\n\n\n<p>It differs from a third-party API, which is essentially an API that someone else has developed (i.e. to access some other resources and for some other use case) and that I&#8217;m using in my <strong>first-party application<\/strong>. I&#8217;m actually going to be Vibe coding third-party API access as part of a CIAM integration, but that&#8217;s a topic for a future article.<\/p>\n\n\n\n<p>Together with a first-party application, a first-party API is owned and maintained by the same organisation. For instance, a mobile banking app developed by a bank, say, will typically access the bank&#8217;s own APIs as a first-party app. <\/p>\n\n\n\n<p>Now, you may be thinking, why use OAuth 2.0 in first-party workflows at all? Well, even if all aspects of a SaaS solution have the same ownership, communication between these will typically happen across the internet. <\/p>\n\n\n\n<p>Once that occurs, an interface is publicly accessible: irrespective of whether or not HTTPS is being employed for data security, the power of any web-based API stems from the fact that it has global accessibility. <\/p>\n\n\n\n<p>Integrating OAuth 2.0 even for wholly first-party scenarios provides a consistent mechanism to protect access from unauthorised parties \u2014 human or otherwise \u2014 in a way that neither <em>Basic Authentication<\/em> nor an <em>API Key<\/em> can easily achieve:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Secure Auth<\/strong>: with no need to share credentials.<\/li>\n\n\n\n<li><strong>Standardised Auth Flow<\/strong>: making security easier to audit and scale.<\/li>\n\n\n\n<li><strong>Token-Based Access<\/strong>: supporting stateless and scalable API design.<\/li>\n\n\n\n<li><strong>Access Control<\/strong>: using the likes of <span class=\"popup-trigger popmake-1623 \" data-popup-id=\"1623\" data-do-default=\"0\">RBAC<\/span> overlaid on scopes and claims.<\/li>\n\n\n\n<li><strong>Standards-based Integration<\/strong>: leveraging profiles and policies enabled via <span class=\"popup-trigger popmake-523 \" data-popup-id=\"523\" data-do-default=\"0\">Social<\/span> identity and (enterprise) <a href=\"https:\/\/discovery.cevolution.co.uk\/ciam\/authenticate\/login\/federation\/\" target=\"_blank\" rel=\"noopener\" title=\"Federation\">Federation<\/a>.<\/li>\n\n\n\n<li><strong>Security Best Practices<\/strong>: enabling features like token rotation, revocation, and session management.<\/li>\n<\/ul>\n\n\n<h3 class=\"wp-block-heading\" id=\"what-is-a-firstparty-application\">What is a First-Party Application?<\/h3>\n\n\n<p>As you&#8217;ve likely already surmised, a <strong>first-party application<\/strong> is one that I&#8217;ve also built&#8230;as opposed to a third-party application, which is one that someone else has built and wants to use to call my API. The distinction is important, as a first-party application\/API combination typically implies a number of important things:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Trusted<\/strong>&nbsp;by the same resource owner.<\/li>\n\n\n\n<li>Often <strong>tightly integrated<\/strong> with internal systems and services.<\/li>\n\n\n\n<li>Can be <strong>deployed across multiple platforms<\/strong> (e.g. <span class=\"popup-trigger popmake-2852 \" data-popup-id=\"2852\" data-do-default=\"0\">web<\/span>, <span class=\"popup-trigger popmake-2862 \" data-popup-id=\"2862\" data-do-default=\"0\">mobile<\/span>, <span class=\"popup-trigger popmake-2866 \" data-popup-id=\"2866\" data-do-default=\"0\">desktop<\/span>).<\/li>\n\n\n\n<li><strong>Shares branding and compliance requirements<\/strong> with <span class=\"popup-trigger popmake-2870\" data-popup-id=\"2870\" data-do-default=\"0\">backend<\/span> systems and the like.<\/li>\n<\/ul>\n\n\n<h2 class=\"wp-block-heading\" id=\"adding-my-api\">Adding my API<\/h2>\n\n\n<p>Ok. Building on the code from my previous article, I&#8217;m going to be using Copilot to help me add some APIs. Now, a <span class=\"popup-trigger popmake-2852\" data-popup-id=\"2852\" data-do-default=\"0\">regular Web Application<\/span> \u2014 which is essentially what a Next.js app is \u2014 will typically have backend routes that function similarly to APIs. <\/p>\n\n\n\n<p>They&#8217;re not typically thought of as being APIs for the fundamental reason that they are typically stateful \u2014 i.e. rely on the application state to determine the likes of security, accessibility, etc. APIs are typically stateless \u2014 what is commonly referred to as RESTful \u2014 where the use of REpresentational State Transfer means that each API call carries with it all the context required.<\/p>\n\n\n\n<p>With Next.js, however, there&#8217;s no reason to prohibit the incorporation of an API as part of the application; an API is essentially a <span class=\"popup-trigger popmake-578\" data-popup-id=\"578\" data-do-default=\"0\">confidential client<\/span>, as in a server-side component, which, of course, Next.js supports. <\/p>\n\n\n\n<p>You don&#8217;t build an API as part of a <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span>, say, nor a (native) <span class=\"popup-trigger popmake-2862\" data-popup-id=\"2862\" data-do-default=\"0\">Mobile App<\/span>, as these are what are typically referred to as <span class=\"popup-trigger popmake-582\" data-popup-id=\"582\" data-do-default=\"0\"><em>public clients<\/em><\/span> and they execute without a backend. Building an API as part of the <code>\"my-nextjs-app\"<\/code> app is beneficial for a number of reasons, not least of which is that it makes it easier when it comes to deployment and management \u2014 e.g. no need to stand up a separate service just to support the various API routes and endpoints. <\/p>\n\n\n\n<p>It also means that I can readily support the use of additional devices\/technology platforms as part of the <span class=\"popup-trigger popmake-2946\" data-popup-id=\"2946\" data-do-default=\"0\">SaaS<\/span> solution I&#8217;m building; in a future article, I&#8217;ll be showing you how I Vibe coded a <span class=\"popup-trigger popmake-1185\" data-popup-id=\"1185\" data-do-default=\"0\">CIAM<\/span>-integrated implementation of an iOS Mobile App that uses the API I&#8217;m building in this article, so stay tuned for more on that.<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-609ba799b916c7de23bfea1b8311643b is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>Whether you&#8217;re building an application in Next.js, an API in Next.js, or some combination of both, <a href=\"https:\/\/vercel.com\" target=\"_blank\" rel=\"noopener\" title=\"\">Vercel<\/a>  \u2014 the creators of Next.js \u2014 offer development\/production hosting services in a 3rd-party SaaS vendor fashion. However, as with a DIY CIAM solution, you can host a Next.js application within your already owned infrastructure should you choose.<\/em><\/p>\n<\/div>\n\n\n\n<p>I&#8217;m essentially creating two APIs, one for the purpose of creating a <em>schedule<\/em> and one for the purpose of managing the <em>events<\/em> that are part of the schedule. This isn&#8217;t an article about the architecture of event scheduling, so I won&#8217;t labour on the reasons for taking that approach. Nor the specifics. <\/p>\n\n\n\n<p>I will, however, show you how I used Copilot to create the schedule API, and also how I used Copilot to add the scheduled event API as a sub-route. This article will focus on just the CRUD \u2014 <em>Create<\/em>, <em>Read<\/em>, <em>Update<\/em> and <em>Delete<\/em> \u2014 operations associated with the former for now, with what I went through applied to the latter at your leisure. <\/p>\n\n\n\n<p>What Copilot came up with as a route for the <em>Schedule<\/em> API, with some of the various endpoints it provided, can be seen below; you can see more of the actual process in the <a href=\"https:\/\/youtu.be\/01Iy9ZPJFnA\" target=\"_blank\" rel=\"noopener\" title=\"\">accompanying video<\/a>, too \ud83e\udd17<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-173fb3dbb464d7e83456e725c86e161b is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>The API I&#8217;m illustrating is an example taken from a B2B SaaS I&#8217;m building, which incorporates a PHP Resource Server backend implementation. So it&#8217;s illustrative of a real-world scenario, but for the purpose of this article, doesn&#8217;t include the full scope of implementation.<\/em><\/p>\n<\/div>\n\n\n\n<div class=\"wp-block-columns are-vertically-aligned-center is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-vertically-aligned-center is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:33.33%\">\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"580\" height=\"436\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20085627\/image-19.png\" alt=\"\" class=\"wp-image-4363\" style=\"box-shadow:var(--wp--preset--shadow--natural)\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20085627\/image-19.png 580w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20085627\/image-19-300x226.png 300w\" sizes=\"auto, (max-width: 580px) 100vw, 580px\" \/><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-vertically-aligned-center is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:66.66%\">\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"1452\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20101540\/image-21-scaled.png\" alt=\"\" class=\"wp-image-4370\" style=\"box-shadow:var(--wp--preset--shadow--natural)\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20101540\/image-21-scaled.png 2560w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20101540\/image-21-300x170.png 300w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20101540\/image-21-1024x581.png 1024w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20101540\/image-21-768x436.png 768w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20101540\/image-21-1536x871.png 1536w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20101540\/image-21-2048x1162.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/figure>\n<\/div>\n<\/div>\n\n\n\n<p>As a first pass, the AI did a fairly reasonable job \u2014 I mean, it created the various endpoints I requested, and after a little re-prompting, it also created the routes I asked for. It certainly took the legwork out of writing the actual lines of code \ud83c\udf89 <\/p>\n\n\n\n<p>Of course, it didn&#8217;t add any security (more on that <a href=\"#security\" title=\"\">below<\/a>) but to be fair, I didn&#8217;t explicitly ask it to; if you&#8217;re watching the <a href=\"https:\/\/youtu.be\/01Iy9ZPJFnA\" target=\"_blank\" rel=\"noopener\" title=\"\">accompanying video<\/a>, you&#8217;ll see (screenshot above) my actual prompt to Copilot mentions nothing about security.<\/p>\n\n\n\n<p>Both context and the language you use are important: when using generative AI, that&#8217;s an aspect worth remembering. I recently came across <a href=\"https:\/\/youtu.be\/wjZofJX0v4M\" target=\"_blank\" rel=\"noopener\" title=\"\">this<\/a> video by <em><a href=\"https:\/\/www.youtube.com\/@3blue1brown\" target=\"_blank\" rel=\"noopener\" title=\"\">3Brown1Blue<\/a><\/em> that neatly illustrates how GPTs \u2014 Generative Progressive\/Pre-trained Transform(er)s \u2014 work in conjunction with LLMs (Large Language Models), and provides some insight into why the way a prompt is phrased is important. <\/p>\n\n\n\n<p>Another example of this can also be seen in my accompanying video, where I attempt to add a new sub-route API for managing scheduled events: because I used the word <code>interface<\/code> the AI thought I was talking about a <em>TypeScrip<\/em>t interface and didn&#8217;t give me what I intended. I had to go back and re-prompt, but I did keep the changes suggested as they did fix some errors in the generated code \ud83d\ude02<\/p>\n\n\n<h3 class=\"wp-block-heading\" id=\"security\">API Security<\/h3>\n\n\n<p>The first pass at generating the API creates something usable, but without any security (a closer look at the implementation generated by using Copilot shows that there&#8217;s absolutely no checking to see who, or what, is allowed access to the resources the API exposes). <\/p>\n\n\n\n<p>There&#8217;s no check to see if the API is being used by something that&#8217;s passed authentication, or not, and there&#8217;s no access control checking either. Let&#8217;s fix that; you can see more on this process in the <a href=\"https:\/\/youtu.be\/01Iy9ZPJFnA\" target=\"_blank\" rel=\"noopener\" title=\"\">accompanying video<\/a>, too. In this article, we&#8217;ll be addressing the former, which will also pave the way to address the latter in more detail at a future point \ud83d\ude01<\/p>\n\n\n\n<div class=\"wp-block-columns are-vertically-aligned-center is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-vertically-aligned-center is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:33.33%\">\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"678\" height=\"232\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24105812\/image-33.png\" alt=\"\" class=\"wp-image-4446\" style=\"box-shadow:var(--wp--preset--shadow--natural)\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24105812\/image-33.png 678w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24105812\/image-33-300x103.png 300w\" sizes=\"auto, (max-width: 678px) 100vw, 678px\" \/><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-vertically-aligned-center is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:66.66%\">\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"1455\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24110230\/image-35-scaled.png\" alt=\"\" class=\"wp-image-4448\" style=\"box-shadow:var(--wp--preset--shadow--natural)\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24110230\/image-35-scaled.png 2560w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24110230\/image-35-300x171.png 300w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24110230\/image-35-1024x582.png 1024w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24110230\/image-35-768x437.png 768w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24110230\/image-35-1536x873.png 1536w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24110230\/image-35-2048x1164.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/figure>\n<\/div>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"building-a-user-interface\">Building a User Interface<\/h2>\n\n\n<p>With the help of AI, I also built a simple page to display a list of schedules and also perform a schedule creation operation; something you can also see in the <a href=\"https:\/\/youtu.be\/01Iy9ZPJFnA\" target=\"_blank\" rel=\"noopener\" title=\"\">accompanying video<\/a>. <\/p>\n\n\n\n<p>Copilot took its familiar approach and created a CSR (Client-Side Rendered) page by default. I was going to ask Copilot \u2014 well, GPT 4.1 to be precise \u2014 to change this, but then I thought, why not add some CSR into what is currently an all SSR (Server-Side Rendered) mix?<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-085ebc69b412acc028a7f1e5d6ca6b64 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>Actually, I did end up CSR&#8217;ing the home page to solve one of the follow-up exercise challenges in my previous article; see <a href=\"https:\/\/github.com\/PeterGFernandez\/my-nextjs-app\/tree\/vibe-coded-authentication\" target=\"_blank\" rel=\"noopener\" title=\"\">here<\/a> in the GitHub repo for more information. However, that page did start its life as being SSR&#8217;d originally \ud83d\ude09<\/em><\/p>\n<\/div>\n\n\n\n<p>CSR vs SSR \u2014 or more specifically, Client-Side API calls vs Server-Side API calls \u2014 do throw up some interesting security implications, but let&#8217;s just go with it for now. <\/p>\n\n\n\n<p>Below is the prompt I gave to Copilot, and what GPT-4.1 ended up producing; you can see it in action in the accompanying video I created for this article. I&#8217;ve not done anything fancy, and you have to navigate to the page manually (as in there&#8217;s no menu\/menu items), but it&#8217;s interesting to see what the AI came up with.<\/p>\n\n\n\n<div class=\"wp-block-columns are-vertically-aligned-center is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-vertically-aligned-center is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:33.33%\">\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"710\" height=\"1116\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/21164044\/image-30.png\" alt=\"\" class=\"wp-image-4432\" style=\"box-shadow:var(--wp--preset--shadow--natural)\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/21164044\/image-30.png 710w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/21164044\/image-30-191x300.png 191w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/21164044\/image-30-651x1024.png 651w\" sizes=\"auto, (max-width: 710px) 100vw, 710px\" \/><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-vertically-aligned-center is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:66.66%\">\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1466\" height=\"1193\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24102844\/image-32.png\" alt=\"\" class=\"wp-image-4444\" style=\"box-shadow:var(--wp--preset--shadow--natural)\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24102844\/image-32.png 1466w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24102844\/image-32-300x244.png 300w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24102844\/image-32-1024x833.png 1024w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24102844\/image-32-768x625.png 768w\" sizes=\"auto, (max-width: 1466px) 100vw, 1466px\" \/><\/figure>\n<\/div>\n<\/div>\n\n\n\n<p>Arguably, one could say that UI generation is de rigueur for a transform process, given that there&#8217;s so much information that can be used from a pre-trained perspective. But remember, with a GPT \u2014 at least a general-purpose one \u2014 we&#8217;re talking about pre-trained transformation from a language perspective, and it&#8217;s not always easy to &#8220;describe&#8221; a UI; I didn&#8217;t even try, so I guess I&#8217;m fortunate that I got even a minimalistic design! \ud83d\ude0e <\/p>\n\n\n\n<p>Maybe if I&#8217;d have used a more specific model tuned to UI generation, I would have obtained a more creative result \ud83e\udd37\ud83c\udffb\u200d\u2642\ufe0f There&#8217;s nothing to stop me from mixing and matching AI technologies after all&#8230;<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-60d8ef4952a07136967945f3997da36c is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>Perhaps because of a precedent already set, GPT-4.1 did include authenticated state checking within my UI, even though I hadn&#8217;t expressly asked it to. I think this is a good example where context (in this case, prior context) also plays an important part. <\/em><\/p>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"calling-the-api\">Calling the API<\/h2>\n\n\n<p>In order to get the UI output (illustrated above), there are a few things that obviously go on under the covers. After I got Copilot to generate the UI, fix for CSR, et al, I took a look at the code it had created and noticed that it was calling my API \u2014 specifically the HTTP <code>GET<\/code> endpoint, to start with, in order to get the list of schedules. <\/p>\n\n\n\n<p>However \u2014 and it&#8217;s a big however, too \u2014 it was supplying the ID Token as the Bearer Token! \ud83d\ude33 I mentioned this to Copilot, and below is how it responded; at least it was gracious about its mistake \ud83d\ude09<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-f6fb1f323a9c0d917761c2c8c14c5a5d is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>For context, an ID Token should <strong>not<\/strong> be used as a <code>Bearer<\/code> token, and you can read more about the respective roles of the ID Token and the Access Token in my article entitled <a href=\"https:\/\/discovery.cevolution.co.uk\/ciam\/2025\/03\/07\/oidc-saml-and-oauth-2-0\/\" target=\"_blank\" rel=\"noopener\" title=\"OIDC, SAML and OAuth 2.0\">OIDC, SAML and OAuth 2.0<\/a>.<\/em><\/p>\n<\/div>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full has-custom-border is-style-default\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"1459\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/19105612\/image-15-scaled.png\" alt=\"\" class=\"wp-image-4334\" style=\"border-style:none;border-width:0px;box-shadow:var(--wp--preset--shadow--natural);object-fit:cover\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/19105612\/image-15-scaled.png 2560w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/19105612\/image-15-300x171.png 300w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/19105612\/image-15-1024x584.png 1024w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/19105612\/image-15-768x438.png 768w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/19105612\/image-15-1536x875.png 1536w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/19105612\/image-15-2048x1167.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/figure>\n\n\n\n<p>The (rectified) Copilot solution is one that you see more often than not in code examples, namely: get an Access Token, store it, and use it to call your APIs. In this case, the Access Token is stored on the session created as part of the login, and I&#8217;ve included a screenshot below of the changes Copilot also suggested as part of <code>route.ts<\/code>. <\/p>\n\n\n\n<p>It&#8217;s not a bad strategy, and for the most part, it works&#8230;especially for low-risk operations where having a longer-lived Access Token is not too much of a concern. What Copilot has done is to abstract getting the access token into a function, which theoretically will enable me to both (a) obtain a new token when the old one has expired, and (b) perform step-up authentication if I were to require an access token with additional scope(s) in the future. <\/p>\n\n\n\n<p>I&#8217;m not going to go into detail in this article \u2014 more to come on this in a future article \u2014 but I did want to point that out as it&#8217;s illustrative of how &#8220;creative&#8221; generative AI can also be&#8230;even when it doesn&#8217;t necessarily intend to be \ud83d\ude0e<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"697\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/19111515\/image-17-1024x697.png\" alt=\"\" class=\"wp-image-4336\" style=\"box-shadow:var(--wp--preset--shadow--natural)\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/19111515\/image-17-1024x697.png 1024w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/19111515\/image-17-300x204.png 300w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/19111515\/image-17-768x522.png 768w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/19111515\/image-17.png 1514w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n<h3 class=\"wp-block-heading\" id=\"scope\"><code>Scope<\/code><\/h3>\n\n\n<p>Aside from typical claims like <code>iss<\/code> (issuer) and <code>sub<\/code> (subject) that can be found in a JWT format <span class=\"popup-trigger popmake-1400\" data-popup-id=\"1400\" data-do-default=\"0\">Access Token<\/span>, a request via <span class=\"popup-trigger popmake-3653\" data-popup-id=\"3653\" data-do-default=\"0\">Authorization Code Flow<\/span> allows a caller to specify custom <code>scope<\/code> as an additional parameter, which typically makes its way into the token via the <code>scope<\/code> claim. <\/p>\n\n\n\n<p>Different implementations often do things differently. Auth0, for example, treats <code>audience<\/code> as a first-class citizen, and forces the definition of a <em>custom API<\/em> \u2014 with a mandatory <em>identifier<\/em> that becomes part of the <code>aud<\/code> claim \u2014 to which <code>scope<\/code> can optionally be attached. Keycloak, on the other hand, forces the definition of a <code>scope<\/code> \u2014 a.k.a. a <em>Client Scope<\/em> \u2014 which can be associated with one or more <code>audience<\/code> values.<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-84309a2b01448c206d19d4354134dc9f is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>An Access Token need not be in JWT format, and in the non-JWT case, Token Introspection is used instead to determine token suitability. Further, the <code>aud<\/code> claim is mostly specific to the <a href=\"https:\/\/openid.net\/specs\/openid-connect-core-1_0.html\" target=\"_blank\" rel=\"noopener\" title=\"\">OpenID 1.0 Core Specification<\/a>, where the JWT format <strong>is<\/strong> mandated for a generated ID Token.<\/em><\/p>\n<\/div>\n\n\n\n<p>Arguably, there are pros and cons to each approach. In the case of Auth0, for example, some say it&#8217;s more intuitive \u2014 i.e. defining an API that has an associated audience makes sense, right? Well, audience isn&#8217;t a specific definition within the <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6749\" target=\"_blank\" rel=\"noopener\" title=\"\">OAuth 2.0 spec<\/a>, largely because OAuth 2.0 is <strong>all about<\/strong> an API rather than the API being some particular characteristic. <\/p>\n\n\n\n<p>Not enforcing the use of <code>scope<\/code>, however, makes it easier for the various routes within an API to dispense with access checking from a consent perspective. Whilst OAuth 2.0 is not an access control protocol per se, implementations of an Authorization Server \u2014 especially those combined with an <span class=\"popup-trigger popmake-415\" data-popup-id=\"415\" data-do-default=\"0\">IdP<\/span> \u2014 often layer the likes of <span class=\"popup-trigger popmake-1623\" data-popup-id=\"1623\" data-do-default=\"0\">RBAC<\/span> on top, which adds access control functionality. If you\u2019re not checking for access in your API, that out-of-the-box capability becomes redundant without you modifying your code!<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-5be6f2df2f9c88d74b6497de54d5e86f is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>To be clear, <strong>not<\/strong> validating <code>scope<\/code> within an OAuth 2.0 Access Token is <strong>not<\/strong> a recommended best practice, as it essentially violates the <span class=\"popup-trigger popmake-4375\" data-popup-id=\"4375\" data-do-default=\"0\">Principle of Least Privilege<\/span> <\/em><\/p>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"keycloak-definitions\">Keycloak Definitions<\/h3>\n\n\n<p>Irrespective of which side of the coin you favour, and which implementation you prefer \u2014 i.e. Auth0, Keycloak, or something else \u2014 you will need to provide some sort of definition to the authorization server. <\/p>\n\n\n\n<p>I&#8217;m using Keycloak, so the images below illustrate the <code>scope<\/code> definitions I&#8217;ve created for my Schedule API. OAuth 2.0 is a <span class=\"popup-trigger popmake-2149\" data-popup-id=\"2149\" data-do-default=\"0\">Delegated Authorization<\/span> protocol, so ordinarily there&#8217;s no provision for whether or not a <code>sub<\/code> \u2014 i.e. a user \u2014 is <strong>allowed<\/strong> to do something or not. <\/p>\n\n\n\n<p>However, access control from an <span class=\"popup-trigger popmake-1623\" data-popup-id=\"1623\" data-do-default=\"0\">RBAC<\/span> perspective is often layered on top, especially where JWT format Access Tokens are used, and the last screenshot shows how this can be defined when using Keycloak. <\/p>\n\n\n\n<p>Of course, an Access Token is also typically used as part of querying other access control services (such as <span class=\"popup-trigger popmake-2333\" data-popup-id=\"2333\" data-do-default=\"0\">ReBAC<\/span> and the like), so it will often participate indirectly in the access control decision-making process. I&#8217;ll be exploring more on that in the future \ud83d\ude01<\/p>\n\n\n\n<div class=\"wp-block-jetpack-slideshow aligncenter\" data-effect=\"slide\"><div class=\"wp-block-jetpack-slideshow_container swiper\"><ul class=\"wp-block-jetpack-slideshow_swiper-wrapper swiper-wrapper\"><li class=\"wp-block-jetpack-slideshow_slide swiper-slide\"><figure><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"1325\" alt=\"\" class=\"wp-block-jetpack-slideshow_image wp-image-4378\" data-id=\"4378\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125114\/image-22-scaled.png\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125114\/image-22-scaled.png 2560w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125114\/image-22-300x155.png 300w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125114\/image-22-1024x530.png 1024w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125114\/image-22-768x397.png 768w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125114\/image-22-1536x795.png 1536w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125114\/image-22-2048x1060.png 2048w\" sizes=\"(max-width: 2560px) 100vw, 2560px\" \/><\/figure><\/li><li class=\"wp-block-jetpack-slideshow_slide swiper-slide\"><figure><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"1321\" alt=\"\" class=\"wp-block-jetpack-slideshow_image wp-image-4379\" data-id=\"4379\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125217\/image-23-scaled.png\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125217\/image-23-scaled.png 2560w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125217\/image-23-300x155.png 300w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125217\/image-23-1024x528.png 1024w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125217\/image-23-768x396.png 768w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125217\/image-23-1536x793.png 1536w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125217\/image-23-2048x1057.png 2048w\" sizes=\"(max-width: 2560px) 100vw, 2560px\" \/><\/figure><\/li><li class=\"wp-block-jetpack-slideshow_slide swiper-slide\"><figure><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"1326\" alt=\"\" class=\"wp-block-jetpack-slideshow_image wp-image-4380\" data-id=\"4380\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125901\/image-24-scaled.png\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125901\/image-24-scaled.png 2560w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125901\/image-24-300x155.png 300w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125901\/image-24-1024x531.png 1024w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125901\/image-24-768x398.png 768w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125901\/image-24-1536x796.png 1536w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20125901\/image-24-2048x1061.png 2048w\" sizes=\"(max-width: 2560px) 100vw, 2560px\" \/><\/figure><\/li><\/ul><a class=\"wp-block-jetpack-slideshow_button-prev swiper-button-prev swiper-button-white\" role=\"button\"><\/a><a class=\"wp-block-jetpack-slideshow_button-next swiper-button-next swiper-button-white\" role=\"button\"><\/a><a aria-label=\"Pause Slideshow\" class=\"wp-block-jetpack-slideshow_button-pause\" role=\"button\"><\/a><div class=\"wp-block-jetpack-slideshow_pagination swiper-pagination swiper-pagination-white\"><\/div><\/div><\/div>\n\n\n\n<p>By default, no audience is associated with a defined <em>Client Scope<\/em> in Keycloak, so nothing is added to the <code>aud<\/code> claim in the Access Token per se. Like I say, it&#8217;s not specifically something defined as part of the <span class=\"popup-trigger popmake-467\" data-popup-id=\"467\" data-do-default=\"0\">OAuth 2.0<\/span> specification. <\/p>\n\n\n\n<p>However, as a best practice, I would recommend having a specific <code>aud<\/code> claim as well as specific <code>scope<\/code> claims as part of an Access Token (at least a JWT format Access Token), as it&#8217;s an additional attribute that can be leveraged from a security perspective. <\/p>\n\n\n\n<p>There are a number of ways to add an audience to a <em>Client Scope<\/em> definition in Keycloak, but the simplest way for now is to use the <code>Audience<\/code> mapper to attach one explicitly; below is a screenshot of how I&#8217;ve done just that:<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-fee461a1779444a1732fda6634d79823 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>Keycloak gives you a lot of flexibility when it comes to adding things to an Access Token. The <code>Audience<\/code> mapper, for example, can be configured so that the claim is added directly to the JWT, or only made available via token introspection, should payload size be a consideration, or if the JWT format is not being used. <\/em><\/p>\n<\/div>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full has-custom-border\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"1323\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20132517\/image-25-scaled.png\" alt=\"\" class=\"wp-image-4385\" style=\"border-radius:20px\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20132517\/image-25-scaled.png 2560w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20132517\/image-25-300x155.png 300w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20132517\/image-25-1024x529.png 1024w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20132517\/image-25-768x397.png 768w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20132517\/image-25-1536x794.png 1536w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20132517\/image-25-2048x1058.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/figure>\n\n\n\n<p>Last but not least, you need to associate the various (Client) <code>scopes<\/code> with the appropriate <code>Clients<\/code>. Below is a screenshot of the <code>nextjs-app<\/code> definition I have in Keycloak to which I&#8217;ve associated the scope definitions created above; I&#8217;ve filtered based on scope name so that it&#8217;s easier to see these various definitions and so that they don&#8217;t get lost among all the other scopes defined to the client. <\/p>\n\n\n\n<p>I&#8217;ve also defined each scope as <code>Optional<\/code> so that it won&#8217;t be included by default (as in it will only be included if explicitly requested).<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full has-custom-border\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"1298\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/23123147\/image-31-scaled.png\" alt=\"\" class=\"wp-image-4440\" style=\"border-radius:20px\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/23123147\/image-31-scaled.png 2560w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/23123147\/image-31-300x152.png 300w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/23123147\/image-31-1024x519.png 1024w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/23123147\/image-31-768x389.png 768w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/23123147\/image-31-1536x779.png 1536w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/23123147\/image-31-2048x1038.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><\/figure>\n\n\n<h2 class=\"wp-block-heading\" id=\"security-considerations\">Security Considerations<\/h2>\n\n\n<p>Resource functionality typically carries with it a certain &#8220;weight&#8221; from a security perspective. For example, whilst being able to read the current total in a bank account might not be desirable, it&#8217;s arguable not as dangerous as being able to transfer funds or withdraw large amounts! <\/p>\n\n\n\n<p>Likewise, being able to read entries from an existing schedule might not be considered as security sensitive as being able to delete or amend existing scheduled events. So the nature of an Access Token often helps define in what context it should be used.<\/p>\n\n\n\n<p>While first-party apps are arguably more trusted, security is always a paramount consideration. Front-end implementation typically executes in what is referred to as a <span class=\"popup-trigger popmake-582\" data-popup-id=\"582\" data-do-default=\"0\"><em>public client<\/em><\/span> context \u2014 i.e. execution in a context which is in the public domain, as in it cannot be easily restricted by both physical and digital access controls. <\/p>\n\n\n\n<p>Back-end implementation, on the other hand, is almost always considered to execute in what is referred to as a <em><span class=\"popup-trigger popmake-578\" data-popup-id=\"578\" data-do-default=\"0\">confidential client<\/span><\/em> context \u2014 namely, a context where certain aspects can be tightly restricted via both a physical and a digital set of access controls.<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-dbff1fed5f8fb1c07390a8d95aa7f3f7 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>Even if you&#8217;re building a <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span> as part of your SaaS solution, you can still benefit from back-end workflows by utilising the Backend-For-Frontend (typically referred to as <span class=\"popup-trigger popmake-2873 \" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span> for short) pattern.<\/em><\/p>\n<\/div>\n\n\n\n<p>From a security perspective, there are always a number of things to consider. I have the option of restricting the visibility of an Access Token so that it&#8217;s only ever &#8220;seen&#8221; from the perspective of back-end usage and never makes its way to the front-end. I also have the option of making an Access Token that carries <code>update<\/code> or <code>delete<\/code> scopes, shorter-lived than one that carries <code>create<\/code> scope, say, with both being shorter-lived than one that carries <code>read<\/code> scope. <\/p>\n\n\n\n<p>For an Access Token expressed as a JWT, this reduces the security impact if a token falls into the hands of a <span class=\"popup-trigger popmake-1754\" data-popup-id=\"1754\" data-do-default=\"0\">bad actor<\/span>. Additionally, I might want to leverage token introspection within more sensitive API routes, where an API will additionally defer to the Authorization Server (i.e. Keycloak) to determine if a token has been revoked. <\/p>\n\n\n\n<p>Here are a few more things to note:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Use <span class=\"popup-trigger popmake-3653 \" data-popup-id=\"3653\" data-do-default=\"0\">Authorization Code<\/span> with PKCE<\/strong>: avoid legacy flows like the <em>Implicit Grant<\/em> or <em>Resource Owner Password Credentials<\/em>, and always choose <em>Authorization Code Flow<\/em> \u2014 ideally with <em><span class=\"popup-trigger popmake-1895\" data-popup-id=\"1895\" data-do-default=\"0\">PKCE<\/span><\/em> \u2014 when it comes to user-oriented auth flows.\n<ul class=\"wp-block-list\">\n<li>Never use <em>Client Secret<\/em> workflow in anything other than a <em><span class=\"popup-trigger popmake-578\" data-popup-id=\"578\" data-do-default=\"0\">Confidential Client<\/span><\/em> context <\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Secure Token Storage:<\/strong> use&nbsp;<code>httpOnly<\/code>&nbsp;cookies or secure session storage for web-based applications, and prefer to use native keychains or encrypted storage for mobile apps and the like.<\/li>\n\n\n\n<li><strong>Avoid transferring an Access Token between client contexts<\/strong>: a token intended for back-end client execution should not be transferred for use by a front-end execution context, and vice versa.<\/li>\n\n\n\n<li><strong>Always validate an Access Token in the API<\/strong>: including <code>scope<\/code> checking, as well as <code>aud<\/code> checking for the most secure experience. <\/li>\n\n\n\n<li><strong>Token Expiry and Refresh<\/strong>: use short-lived Access Tokens and utilise <em><span class=\"popup-trigger popmake-4495 \" data-popup-id=\"4495\" data-do-default=\"0\">Refresh Tokens<\/span><\/em> (more on those in a future article) to maintain a balanced security posture.<\/li>\n\n\n\n<li><strong>Revocation and Logout<\/strong>: support token revocation and propagate logout across app sessions \u2014 again, more on those in future articles.<\/li>\n\n\n\n<li><strong>TLS Everywhere<\/strong>: In a production environment, all communication between Clients, <span class=\"popup-trigger popmake-415 \" data-popup-id=\"415\" data-do-default=\"0\">IdP&#8217;s<\/span>, an Authorization Server, APIs, etc. should always be conducted over HTTPS.<\/li>\n<\/ul>\n\n\n<h2 class=\"wp-block-heading\" id=\"maintainability\">Maintenability<\/h2>\n\n\n<p>To date, my experience with Vibe Coding has taught me that you will likely get some duplication, especially where full-context visibility isn&#8217;t apparent (such as across AI chat sessions and the like). This isn&#8217;t unlike the situation that can arise where more than one developer works on the same piece of code&#8230;particularly when they&#8217;re not doing so concurrently, and where there is unfamiliarity with the code base. <\/p>\n\n\n\n<p>All of these situations create maintainability issues going forward, and so to minimise technical debt, it&#8217;s often worth going back over Vibe-generated code periodically to address inconsistencies. <\/p>\n\n\n\n<p>For example, you can see in the screenshots below how the Vibe-coded additions to the Vibe-coded <code>POST<\/code> calls use both the internally Vibe-coded <code>withAuth<\/code> and the externally Vibe-coded <code>verifyAccessToken<\/code> \u2014 the latter of which essentially does the same as the former, thus duplicating functionality.<\/p>\n\n\n\n<div class=\"wp-block-group is-vertical is-content-justification-center is-layout-flex wp-container-core-group-is-layout-b6c1f246 wp-block-group-is-layout-flex\">\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1966\" height=\"1030\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24132837\/image-36.png\" alt=\"\" class=\"wp-image-4461\" style=\"box-shadow:var(--wp--preset--shadow--natural)\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24132837\/image-36.png 1966w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24132837\/image-36-300x157.png 300w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24132837\/image-36-1024x536.png 1024w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24132837\/image-36-768x402.png 768w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24132837\/image-36-1536x805.png 1536w\" sizes=\"auto, (max-width: 1966px) 100vw, 1966px\" \/><figcaption class=\"wp-element-caption\">Vibe-coded <code>POST<\/code> using both&#8230;<\/figcaption><\/figure>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1970\" height=\"1026\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24132937\/image-37.png\" alt=\"\" class=\"wp-image-4462\" style=\"box-shadow:var(--wp--preset--shadow--natural)\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24132937\/image-37.png 1970w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24132937\/image-37-300x156.png 300w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24132937\/image-37-1024x533.png 1024w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24132937\/image-37-768x400.png 768w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24132937\/image-37-1536x800.png 1536w\" sizes=\"auto, (max-width: 1970px) 100vw, 1970px\" \/><figcaption class=\"wp-element-caption\">&#8230;the inline generated <code>withAuth<\/code>&#8230;<\/figcaption><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1972\" height=\"1030\" src=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24133012\/image-38.png\" alt=\"\" class=\"wp-image-4463\" style=\"box-shadow:var(--wp--preset--shadow--natural)\" srcset=\"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24133012\/image-38.png 1972w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24133012\/image-38-300x157.png 300w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24133012\/image-38-1024x535.png 1024w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24133012\/image-38-768x401.png 768w, https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/24133012\/image-38-1536x802.png 1536w\" sizes=\"auto, (max-width: 1972px) 100vw, 1972px\" \/><figcaption class=\"wp-element-caption\">&#8230;and the externally generated <code>verifyAccessToken<\/code><\/figcaption><\/figure>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<p>The best way to address this is to refactor the code so that only one function is used; as a seasoned developer, I&#8217;d opt for keeping the externally created <code>token.ts<\/code> as it also provides flexibility to add additional functionality without disruption (in this case, additional <code>createRemoteJWKSSet<\/code> caching, for example, could be added to reduce execution per interaction). <\/p>\n\n\n\n<p>I&#8217;d also refactor to relocate <code>token.ts<\/code>, because it&#8217;s not just specific to the <code>schedule<\/code> API route. I&#8217;ve done some &#8220;human&#8221; rework, and you can find those as part of the GitHub repo I&#8217;ve created; you may like to tackle some refactoring of your own for additional maintainability \ud83d\ude01<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-cd3689ada32a8b2cecb0ccfa51114cd7 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>Testing is something that&#8217;s also important for maintainability, especially as the code base grows, and I&#8217;ll be looking at opportunities for Vibe coded testing from a CIAM perspective in a future article \ud83e\udd17<\/em><\/p>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"exercises\">Followup Exercises<\/h2>\n\n\n<p>If you&#8217;re following along in the <a href=\"https:\/\/youtu.be\/01Iy9ZPJFnA\" target=\"_blank\" rel=\"noopener\" title=\"\">accompanying video<\/a>, then you can see the final outcome \u2014 as well as everything else discussed here \u2014 in action. In my previous article, <a href=\"https:\/\/discovery.cevolution.co.uk\/ciam\/2025\/05\/16\/vibe-coded-authn\/\" title=\"Vibe Coding Authentication via Authorization Code Flow\"><em>Vibe Coding Authentication via Authorization Code Flow<\/em><\/a>, I also left you with a few follow up exercises, and you can check out the GitHub repo <a href=\"https:\/\/github.com\/PeterGFernandez\/my-nextjs-app\/tree\/vibe-coded-authentication\" target=\"_blank\" rel=\"noopener\" title=\"\">here<\/a> to compare with my solutions and see how you got on \ud83d\ude0e <\/p>\n\n\n\n<p>Of course, it wouldn&#8217;t be the same if I didn&#8217;t leave you with a few more follow-up exercises for you to consider and &#8220;get your teeth into&#8221;, at your leisure \u2014 once again, ideally working with Copilot (or your preferred AI of choice) collaboratively to try and solve the challenges:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Secure Schedule Update via <code>PUT<\/code><\/strong>. Vibe code <em>update<\/em> to a schedule in a similar fashion to the way implementation for <em>create<\/em> processing was vibe coded. Remember, this is arguably another of the more security-sensitive operations, so think about ways to minimise potential security impact. <\/li>\n\n\n\n<li><strong>Implement Schedule <code>DELETE<\/code><\/strong>. This API route was, for some reason, completely left out of the code generated by the AI \ud83e\udd37\ud83c\udffb\u200d\u2642\ufe0f I&#8217;d be interested in hearing your thoughts on why that might have been, and also interested to see what you come up with as a vibe-coded solution. Again, this is arguably another of the more security-sensitive operations, so consider ways to minimise potential security impact. <\/li>\n\n\n\n<li><strong>Add a menu to switch between pages<\/strong>. I mean, a menu makes it so much easier to navigate around, right? \ud83d\ude09 It&#8217;ll be fun to see what sort of menu you can come up with based on your vibe-coding prompt to the AI \ud83d\ude0e<\/li>\n\n\n\n<li><strong>Create a consistent experience when not authenticated<\/strong>. Going back to some of the considerations I touched upon in maintainability (<a href=\"#maintainability\" title=\"\">above<\/a>), I&#8217;ve also noticed that there isn&#8217;t a consistent UX for what transpires when a user is not already logged in and navigates to a protected page. Depending on the path taken, the user can be presented with a message prompting them to log in, all the way to actually being presented with a login prompt. It would be nice if this were a consistent experience, and I&#8217;ll be interested to see what you come up with \ud83e\udd17 <\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Microservice architectures, interconnected applications, and even AI \u2014 all enabled by APIs \u2014 mean secure access has become an increasingly complex challenge. See how Vibe coding with Copilot, NextAuth.js and Keycloak can help implement a solution using OAuth 2.0 that&#8217;s able to meet the challenge. <\/p>\n","protected":false},"author":1,"featured_media":4395,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"authenticate":"","authentication":"","authenticatedMethod":"","authenticatedMember":"","authorizedPermissions":[],"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"How Vibe coding with Copilot, NextAuth.js and Keycloak can help implement a solution using OAuth 2.0 that's able to protect your APIs.","jetpack_publicize_feature_enabled":false,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"enabled":false},"version":2},"_links_to":"","_links_to_target":""},"categories":[8],"tags":[80,22,28,71,70,20,69,68],"class_list":["post-4092","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-authorization","tag-ai","tag-ciam","tag-keycloak","tag-next","tag-nextauth","tag-oauth2","tag-vibe","tag-vibe-coding"],"aioseo_notices":[],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/06\/20134438\/create-a-highly-detailed-high-resolution-featured-image-for-a-blog-2.png","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/posts\/4092","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/comments?post=4092"}],"version-history":[{"count":113,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/posts\/4092\/revisions"}],"predecessor-version":[{"id":5639,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/posts\/4092\/revisions\/5639"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/media\/4395"}],"wp:attachment":[{"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/media?parent=4092"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/categories?post=4092"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/tags?post=4092"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}