Introduction
If you’re reading this article, there’s a very good chance you’re a developer who got told by someone from management or the security team: “We need Entra login, we need SSO. Can you handle it?” And of course, like any normal developer, you say “sure” while actually having no idea what’s waiting for you.
First step: you open the official Microsoft Entra documentation.
Second step: you realize the documentation has more pages than your thesis, it talks about things you’re not sure are protocols, rare diseases, or new Marvel series — and of course, it’s all written in some semi-academic C5 English. Meanwhile, you’re here for one single question:
“How the hell do I implement this in my Go code?”
After a few minutes of fighting the documentation, a realization hits you:
“Someone surely already wrote a how-to implementation of this on the internet.”
And that’s true — but only technically. After 2–3 articles you realize most authors write the most superficial “hello world” possible: they explain where to click in the Azure portal and then copy-paste 10 lines of code without a single explanation. Concepts like the SAML protocol, redirect flow, assertion validation, certificate trust, or role claim mapping? Absolutely not. Why fewer and fewer authors write quality texts and tutorials is a topic on its own, and maybe we’ll cover it in some other article.
After a disappointing Google search, in desperation you turn to artificial intelligence.
You type the question, and AI delivers a magical snippet:
“Here’s how to implement Entra SAML login in Golang.”
Copy-paste → doesn’t work.
And then you realize even the poor AI doesn’t know how to implement this. How would it, when almost nobody on the internet writes about this in a normal, practical way? SAML + Entra + Go is such a knowledge gap that even the most advanced models are forced to hallucinate because they simply don’t have enough high-quality examples.
And that brings us to this article. I don’t care about engagement, and I don’t care whether you find this boring or not. I want to explain this properly, in full detail, the way I wished someone had explained it to me.
The goal is simple:
So that after reading this article you not only know how to implement Entra login, but also why it’s a good idea, what’s happening under the hood, and how to avoid all the traps the official documentation takes for granted.
Quick Disclamer
If you’re a “get shit done” type of developer who doesn’t need additional theory to implement something, feel free to skip straight to the implementation section. That said, I still recommend everyone read the theoretical part, as it summarizes the most important concepts around federated authentication, SSO, Microsoft Entra, and closely related terms and ideas. With that knowledge, you’ll find it much easier to implement similar systems in future projects.
Personally, I’m a big believer in understanding the background of every implementation. I believe that knowing why something is good, why something is bad, or why one solution is better than another is what separates an average developer from a truly good one. Theory might feel boring at first, but in the long run it’s a skill that opens the door to more complex systems and gives you greater control over what you’re building.
- Introduction
- Quick Disclamer
- What Is This All About (OPTIONAL)
- What Is Entra (OPTIONAL)
- Implementation
- Conclusion
What is this all about (OPTIONAL)
Federated Authentication
To truly understand what Entra is at its core, how it’s used, and what its benefits are, it’s best to start with the umbrella concept under which this technology actually lives. That umbrella is called federated authentication and under it, besides Entra, you’ll also find platforms like Okta, Google Identity, OneLogin, and many others.
Federated authentication is a concept that fits perfectly into the paradigm:
“DO NOT REINVENT THE WHEEL”
As the name suggests, it’s an approach where we, as developers, do not handle authentication ourselves, but instead delegate it to a specialized system. That means we don’t touch the database, we don’t compare hashed passwords, we don’t verify MFA tokens, and we don’t try to play security experts ourselves — someone else does all of that for us.
In the context of this concept, it’s important to understand two terms: Identity Provider and Service Provider.
A Service Provider (SP) is our application. It’s a piece of software that offers some kind of service. For a user to use that service, they need to authenticate. However, our application has no idea who the user is or whether they’re allowed to use the service. The Identity Provider (IdP) is the one that knows who the user is, verifies their identity, and guarantees their authenticity. In our case, the Identity Provider is Entra.
If we want to explain this visually:
Our application is like a waiter in a café — they serve drinks to customers, but they don’t decide who is allowed to be served.
Entra is the bouncer at the entrance — it checks identities, decides who can get in, and once a guest is inside, the waiter simply knows the person has been verified and can be served.
In other words: the SP serves functionality, the IdP serves identity.
Here’s how this looks in practice:
A user opens your application and clicks on Entra login (or any other IdP login). Your application then redirects them to the identity provider’s login page, where the user is authenticated. After a successful login, the IdP contacts your application in the background and sends it a signed confirmation that the user has indeed been authenticated. Based on that callback, your application then creates its own session or uses a session already created by the IdP (depending on the implementation), and only then lets the user in.
If you still don’t have a clear picture of how the entire flow actually works — despite my obviously top-tier mentoring skills — maybe this image will finally put all the pieces together.
SSO
Another term that often comes up in the context of federated authentication is SSO — Single Sign-On. Single Sign-On is simply a consequence of federated authentication. Federated authentication is the mechanism by which our application delegates authentication to an external Identity Provider like Entra. Once a user signs in with that provider, they obtain a valid session, and all other applications that trust the same provider can reuse that session without requiring the user to log in again. In short: federation is the process, and SSO is the user experience that results from it.
In the context of an identity provider, SSO doesn’t just enable seamless access to applications that trust that provider — it also gives the company an additional layer of control. Through the identity provider, the company can define which employee is allowed to access which application, based on the groups that user belongs to — groups that are defined within the identity provider’s own settings.
For example, users who belong to the Finance group can automatically be granted access to a financial application, while users in the Engineering group get access to internal developer tools. If an employee moves from one group to another, access to applications changes automatically, without you as a developer having to touch the code or introduce any special logic.
This might sound a bit abstract at first, but through a concrete Entra example later on, everything will fall into place. And so you’re not left hanging until then, here’s an image that should help lay the groundwork for understanding SSO — just in case my extremely good explanation didn’t quite do the trick.
Protocols
A very important thing to understand before we go any deeper is that SSO is not magic — it’s built on top of standardized protocols. Just like the internet relies on clearly defined communication protocols such as HTTPS, GraphQL, or gRPC, there are also protocols that define how identity is transferred, how authentication is delegated, and how applications establish trust with an IdP.
In the world of SSO, the two most important protocols are:
- SAML (Security Assertion Markup Language)
- OIDC (OpenID Connect)
SAML and OIDC solve the same core problem — how to prove to an application that a user has genuinely authenticated with an external identity provider. The difference is how they do it, where they’re typically used, and what assumptions they make about the environment they operate in.
SAML is older, heavier, and more serious. It was built for enterprise environments where the rule is “nothing is simple, everything is clogged with firewalls, proxies, and ancient browsers — but it has to work every single time.” That’s why it relies on XML, signed assertions, and very strict identity semantics. It was designed to survive bizarre network configurations, decades-old systems, and weapons known as Internet Explorer. In short, SAML does not assume the world is a clean and orderly place — it assumes the exact opposite.
OIDC, on the other hand, is the modern kid on the block — lighter, faster, and far more pleasant to work with. It uses JSON, JWTs, modern redirect flows, and fits perfectly into web apps, mobile apps, SPAs, and pretty much everything built in the last ten years. The problem is that OIDC assumes it lives on a modern, healthy internet: modern browsers, sane networks, clean redirects, and tokens that can travel where they’re supposed to. In enterprise environments, this is often not the case. A proxy rewrites your redirect, a firewall kills the code flow, an old browser has no idea what to do with modern security settings — and the entire OIDC flow collapses before it even starts. In those conditions, SAML wins without much effort because it doesn’t rely on the client, doesn’t depend on JavaScript, doesn’t assume an ideal network path, and has a much more robust trust model.
On the flip side, OIDC shines exactly where SAML feels like a mastodon. If you’re building a web or mobile application, want a clean login flow, need tokens that move elegantly between microservices, and want something that integrates easily — OIDC is the absolute winner. In those scenarios, SAML is simply too heavy and unnecessarily complex. OIDC gives you everything you need without the enterprise baggage. I believe this is one of the reasons why there are plenty of guides on implementing Entra with OIDC in Golang, while SAML doesn’t get the same treatment.
When it comes to the relationship between OIDC and OAuth2.0, there’s a classic source of confusion: people say “OAuth2 login” when they actually mean “OIDC login.” OAuth2 is not an authentication protocol at all — it’s an authorization framework. It gives you permission to access someone’s resources, but it does not tell you who the user is. That’s exactly why OIDC exists: it adds identity on top of what OAuth2 already provides, resulting in a modern login flow. In other words, OIDC stands on the shoulders of OAuth2.0, reuses its flows, but adds the one thing OAuth alone doesn’t provide — who actually logged in.
In our case, the focus will be on SAML, because that’s the protocol Entra uses for enterprise SSO integrations — and, perhaps more importantly, because of the lack of proper documentation.
Tell Me Why !
Alright, we’ve roughly explained what federated authentication is and the key terms closely tied to that concept, but you might still be wondering:
“Okay, but why is this so important? Why would I even want to implement this in my application?”
Maybe you’re one of those overconfident developers who believes their auth flow is unbreakable, that no exploit exists that could take it down, and that you’ve got that giga-chad mindset: “Who could possibly do this better than me?!” — better even than the 10x developers Microsoft pays to work exclusively on identity and security.
And you know what?
You might even be right.
But — and this is crucial — this isn’t actually about you.
That’s something I personally only realized halfway through implementing Entra. The entire time, I was looking at the concept from a developer’s perspective: what it means for me, how much work it saves me, what I gain or lose. It turned out that this was completely the wrong way to look at it.
In this case, you have to look at things from the perspective of your client — the company or the person who will actually be using the software you’re building.
And that’s where we get to the main reason why federated authentication is actually so cool.
Imagine you’re an employee at company X, which uses three different software systems. Each one has its own authentication flow. That means you have to:
- register three times
- log in three times
- remember three passwords
- go through three password recovery flows
In theory, a password recovery flow isn’t a big deal. You click “Forgot password”, get an email, reset your password, and move on with your life. The problem starts when you realize you’re not doing this once — you’re doing it three times, because every application has its own login, its own reset flow, and its own set of rules.
In the best possible scenario, you’re a responsible adult who uses different passwords for different applications, so you’ve only forgotten one. Congratulations — you’re a statistical anomaly. In the real world, most people use the same password everywhere until their password manager breaks or the security team forces a rotation (yes, I’m talking about you).
And suddenly, what was supposed to be a trivial action turns into a small ritual: reset the password here, reset it there, another email, another token, another “password must contain at least one hieroglyph”.
That’s the point where you realize the problem isn’t the forgotten password — it’s the fact that there are too many of them.
Now imagine the alternative:
All three systems use the same authentication flow, the same identity, the same provider.
You log into one — the other two automatically know you’ve already authenticated and let you in without any additional login.
A user experience appreciated even by the average, moderately tech-literate “average Joe”.
And that’s what you, as a developer, enable for your client by using federated authentication:
- a simpler life (better user experience)
- centralized access management
- a more secure system with fewer potential points of data leakage
And for yourself, you gain:
- not having to implement password recovery flows
- not having to implement MFA
- not having to store passwords
- not having to worry about hashing, rotation, or compromise
- not having to invent your own security system, which will almost certainly be worse than an enterprise-grade solution
If you want to do all of that yourself — go ahead. I even respect that level of masochism.
But in today’s world, besides raw skill, what’s valued is practicality, efficiency, and speed — and time, as you well know, is money.
If you’ve made it this far, you should now have a much clearer picture of why federated authentication exists, why SSO isn’t just a buzzword for managers, and why in certain cases it simply doesn’t make sense to reinvent your own authentication wheel. Authentication isn’t just “check a password and let the user in” — it’s an entire ecosystem of security, user experience, and organizational policies that your application alone will never be able to handle as well as a specialized system.
And that’s where Microsoft Entra enters the scene.
Entra isn’t “just another login service” among millions of others — it’s a full-fledged identity provider built for the enterprise world, designed to understand its problems and to use a protocol (SAML) created specifically for those conditions. If you want a stable, battle-tested, industry-standard authentication solution that works even in the strangest networks, with the oldest browsers and the strictest security policies, Entra is the tool for you.
WHAT IS ENTRA (OPTIONAL)
General
There’s no need to go into a long-winded explanation of what Entra is and what it does — it’s enough to say that Microsoft Entra is an identity provider developed by, believe it or not, Microsoft. If you skipped the previous section, I kindly ask you to show at least a little respect for my typing and extensive use of ChatGPT (FOR TEXT EDITING PURPOSES) and go back to read it, because it provides the broader context without which this section doesn’t really make sense.
It’s important to know that Entra used to be called Azure AD. These are essentially the same thing under a different name. So yes — Entra lives on the Azure platform and builds on everything Azure has been offering for years in the areas of identity, SSO, and enterprise integrations.
Since I’ve already explained what identity providers are, this section about Entra will focus on things that you, as a developer, most often don’t see, don’t know, and maybe don’t even care about in day-to-day work — but which I personally think are very useful to understand. In other words, you’re building an application for a company, that application will use Entra login, and Entra itself is configured by the client’s IT admin. Understanding what Entra looks like from their perspective and what they configure there will help you clearly see where your implementation starts and where it ends.
There’s one more thing I have to emphasize here — and I really can’t emphasize it enough. Over the course of my career, I’ve run into various project managers who, to put it mildly, weren’t always fully aware of the technical details. And let’s be honest — we developers aren’t exactly world champions of communication either. That combination sometimes ends up feeling like a game of chinese whispers.
That’s exactly why it’s important to understand both what’s happening in your code and what’s happening on the client’s side in their identity system. When you understand both sides of the story, you can much more precisely articulate what you need, who you need it from, and why you need it. And trust me — you will need data from their IT admin, because Entra simply doesn’t work without configuration on their side.
And as we’ve already said: time is money, efficiency is the goal — not just in code, but in communication as well. So the next time you get all the required information in one go, without endless back-and-forth diplomacy through a project manager with a verified LinkedIn checkmark, feel free to think of me and send a mental “thank you.”
Azure Platform
So, the Azure platform is the place where Entra lives, and also the place where the IT admins and security admins of your client reside (the company you’re building the application for and implementing Entra login for). When one of those admins logs into the Azure portal, they’re greeted by an interface that looks roughly like this.
In the image, some of the tabs are highlighted for informational purposes, but the most important one is actually the “Enterprise apps” tab. So let’s start digging into the Entra interface.
Let’s say company X uses Entra SSO to access a large number of its internal services and is now hiring a new employee: B. Simon. For B. Simon to be able to access any of those services, the IT admin must first add him to Entra as an employee. This is done in the Users tab, which looks roughly like this:
B. Simon was hired to help increase sales in the company, so it’s only logical that he becomes a new member of the Sales team. The IT admin therefore adds him to the Sales group in Entra.
Why is this useful?
Because grouping users allows you to automatically assign certain capabilities or access rights to an entire group, without having to manually assign permissions to each individual user one by one.
It’s important to emphasize this: your application’s permissions are not assigned inside Entra. Entra only categorizes users using groups and roles — and it’s precisely this categorization that allows you, as a developer, to build a permission system inside your application very easily.
So, when B. Simon logs into your application via Entra, you can see:
“Ah, this user belongs to the Sales group.”
Based on that, you can decide:
- whether he’s allowed to access your application at all (for example, if it’s an app for sales forecasting)
- whether he’s allowed to see certain parts of the application, such as the Sales tab
- whether he’s allowed to perform certain actions within the system
In other words, permissioning is always your responsibility — it’s defined by your application and the business rules you agree on with the client. Entra simply gives you a clean and reliable classification of users, and you decide what that classification means in practice. The only thing Entra might directly block is access to a specific service — for example, B. Simon, who is a member of the Sales team, might not be allowed to access an application used for design.
The previously mentioned grouping of employees happens in the Groups tab, where we can see the group that our B. Simon belongs to.
The previously mentioned grouping of employees happens in the Groups tab, where we can see the group that our B. Simon belongs to.
Here, the IT admin can configure which employee belongs to which group, the roles within that group, and which applications the group is allowed to access.
This is where the part that may not directly concern you comes to an end — but again, it’s never a bad idea to know how things look on “the other side of the screen.” Understanding this context often saves a lot of nerves later during implementation.
Now we can move on to the tab that actually matters to us and from which all the data required to connect our application (the Service Provider) with our client’s Entra (the Identity Provider) comes.
That tab is — Enterprise applications.
So imagine that company X, where B. Simon works, uses multiple applications. In this tab, those applications are registered so they can be accessed via federated authentication. In the image, we see our Test application (created solely for demonstration purposes).
So here we have an overview of all applications, and if we were to open the interface for any of them, we’d see a range of configuration options related to that application. Here, the IT admin can, for example, define which employee groups are allowed to access the application — but most importantly for us as developers, this is also where the SSO settings are configured.
Once you enter the SSO settings, you finally arrive at what actually matters to us as developers — concrete data that is trivial on its own, but when combined with a lack of communication skills on your side, limited technical understanding from the project manager, and a questionable “give-a-fuck” capacity from company X’s IT admin, can very quickly turn into a source of unimaginable amounts of wasted time, nerves, and energy.
Of course, for security reasons I’ve hidden the actual values, but the hidden data is exactly what’s crucial for us as developers.
-
Identifier (Entity ID)
This is simply the identifier of your application. Entra uses it to know which application it’s talking to and to whom it should issue the SAML assertion. No philosophy here — it’s the “name” of your application in the SAML world.
-
Reply URL (Assertion Consumer Service URL)
This URL is very important. Remember the part where the identity provider authenticates the user and then sends your application information about who that user is?
This is the URL where that information is delivered.
In other words, the Reply URL is the endpoint to which Entra sends the callback request after a successful authentication. This is an endpoint you must implement in your application.
As for the URL itself — it’s arbitrary. The IT admin can define it and send it to you, or (which I personally prefer) you implement the endpoint first and then simply tell the IT admin which URL to enter into Entra.
In my opinion, a reasonable example would be:
https://application_base_url/api/auth/entra/callbackHowever you choose to do it, the important thing is that the value configured as the Reply URL in Entra exactly matches the endpoint you’ve implemented.
-
App Federation Metadata URL
This URL also needs to be provided to you by the IT admin. It looks “weird”, but don’t worry — you don’t need to care how it’s generated or what exactly it contains. It’s an endpoint your application calls to fetch additional SAML information (certificates, endpoints, bindings, and so on).
In practice, the IT admin will almost always leave the default metadata URL in place and simply forward it to you. Your task is simple: ask for it, store it, and use it without overthinking it.
And that’s it. In this section, we intentionally looked at Entra from the perspective of the IT admin and the client — not because you need to memorize all of this, but because it’s useful to understand where the data you’ll use in your code comes from and why you don’t always get it on the first try. Entra is much more than “just another login button”, and once it enters an enterprise context, people, processes, and configurations come into play — things you, as a developer, don’t control.
The good news is that from the developer’s perspective, things are actually much simpler than the Azure UI might suggest. If you have the Entity ID, the Reply URL, and the Federation Metadata URL, you have everything you need to connect your application to Entra. Everything else is just noise.
From this point on, we stop clicking around the Azure portal and move to where we’re comfortable — the code. In the next section, we’ll go step by step through how to implement SAML login with Microsoft Entra ID in a Go application, how to handle the callback, validate the SAML assertion, and extract a user that can be used normally inside the application.
It’s time to leave theory and UI behind and finally write the code you opened this article for in the first place.
IMPLEMENTATION
Service Provider
Before we dive into the implementation, I need to emphasize one thing: I’m not a frontend developer, and I won’t be going into details on how to implement anything on the frontend. The focus of this section is strictly backend — what needs to be done, which endpoints you need to implement, and how to handle the SAML flow.
Honestly, for the sake of your mental health, I hope you’re not simultaneously trying to figure out Entra implementation on the backend and how to implement all of this on the frontend as well (if you know what I mean).
Before we get into the actual implementation, it’s a good idea to set some technical constraints so you can more easily follow the instructions and understand which parts are up to you.
- I won’t get into your architectural choices — I’ll show Entra implementation using my own architecture, just so less experienced developers don’t get scared and start wondering what this “scary” AuthController is supposed to be
- I’m using the Echo web framework, but the same principle applies to any other framework (Gin, Gorilla), or even the standard Go
net/httpimplementation, with minimal adjustments - For handling the SAML flow we will be using the
crewjam/samlpackage
In this story, the main character is the *samlsp.Middleware object. This object is the key to handling the entire SAML flow, because it contains the methods for validating (asserting) the SAML response we receive from the identity provider, as well as establishing and maintaining communication with it — in our case, with Entra. In my example, I’ll embed this object inside my AuthController and I’ll call it ServiceProvider. Throughout the rest of the text, I’ll refer to this object by that name.
package controllers
import (
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
"github.com/labstack/echo/v4"
"net/http"
"net/url"
)
type AuthController struct {
ServiceProvider *samlsp.Middleware
}
func NewAuthController(ctx context.Context) *AuthController {
.
.
.
}
When initializing the ServiceProvider object, you need to embed a *saml.EntityDescriptor into it. This object describes our application in the SAML world and contains all the information required for Entra and our application to understand each other.
A key part of that mutual understanding is certificates.
To obtain the EntityDescriptor, we use the App Federation Metadata URL — that famous URL you got from the IT admin. During the initialization of the entity descriptor, the crewjam/saml package literally sends a GET request to Entra to fetch the required data from that endpoint (certificates, endpoints, bindings, and other SAML metadata) without which the SAML flow simply wouldn’t work.
I mentioned certificates — and they are an extremely important part of the trust relationship between our application and Entra.
The SAML response that Entra sends back to your application (a POST request from our application’s perspective) is cryptographically protected. That response is always signed, so that the ServiceProvider can be sure the data really came from Entra and wasn’t modified along the way.
For our application to validate that signature, it needs the Identity Provider’s public certificate (Entra’s) — the certificate Entra uses to sign its SAML responses. And that’s exactly the certificate our ServiceProvider gets during the initial GET request to the App Federation Metadata URL.
In other words, the metadata endpoint acts as the source of truth for Entra’s side of the story: it provides the public keys, endpoints, and other information necessary for our application to securely accept and process the SAML response. Without this step, the ServiceProvider would have no way to cryptographically confirm the authenticity of the data it receives — and without that, the SAML flow would make no sense.
I won’t go too deep into the internals of the package — if you want to see what exactly happens under the hood, feel free to check the crewjam/saml source code. For the purposes of this integration, it’s enough to understand that the metadata URL is the source of truth for all SAML settings coming from Entra’s side.
func NewAuthController(ctx context.Context) *AuthController {
// It's is important that this string doesn't contain quotes
cleanIdpMetadataURL := os.Getenv("ENTRA_APP_FEDERATION_METADATA_URL")
cleanIdpMetadataURL = strings.Trim(cleanIdpMetadataURL, `"'`)
idpMetadataURL, err := url.Parse(cleanIdpMetadataURL)
if err != nil {
// handle error however you want
}
// Load IdP metadata, create *saml.EntityDescriptor object
idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient, *idpMetadataURL)
if err != nil {
// handle error however you want
}
.
.
.
}
Just as Entra signs its responses, our application can also sign its own requests. I’m deliberately emphasizing the word can, because this step is optional. Whether your application sends signed SAML requests depends entirely on how the IT admin has configured the SSO settings in Entra.
Within the SSO settings, there is an option to enable or disable verification of requests coming from the Service Provider. If this option is disabled, Entra will accept unsigned AuthnRequests; if it’s enabled, every request must be cryptographically signed in order to be accepted.
Here’s a quick reminder of what that looks like in the Azure interface:
In the example shown in the image, request verification is set to Not required, which means signing is not mandatory. Still, in the next section I’ll also show the code you’ll need in case the IT admin decides to require signed SAML requests — because that’s a very common requirement in stricter enterprise environments.
First, you need to generate your own certificate. You can do that with the following command:
openssl req -x509 -newkey rsa:2048 -keyout saml_key.cer -out saml_cert.cer -days 365 -nodes -subj "/CN=localhost"
This command generates your private key and public certificate as files. However, it’s not a good practice to keep sensitive files like these in your repository. There are many ways to handle secrets safely without letting them end up in version control, but I personally stick to a simple and proven approach: I convert the file contents into Base64 and store them in environment variables.
You can do that very easily with these commands:
base64 -i saml_cert.cer
base64 -i saml_key.cer
The output of each command is a Base64 string which you then just paste into your .env file under the appropriate keys — for example SAML_CERT and SAML_KEY. This way the certificate and private key aren’t hardcoded in your app, they’re not sitting in the repository, and they’re easy to replace or rotate when needed.
Inside the application, we then read those values from environment variables, decode them, and use them to initialize the Service Provider. Here’s what that looks like in Go code:
package controllers
import (
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
"github.com/labstack/echo/v4"
"net/http"
"net/url"
)
type AuthController struct {
ServiceProvider *samlsp.Middleware
}
func NewAuthController(ctx context.Context) *AuthController {
// It's is important that this string doesn't contain quotes
cleanIdpMetadataURL := os.Getenv("ENTRA_APP_FEDERATION_METADATA_URL")
cleanIdpMetadataURL = strings.Trim(cleanIdpMetadataURL, `"'`)
idpMetadataURL, err := url.Parse(cleanIdpMetadataURL)
if err != nil {
// handle error however you want
}
// Load IdP metadata, create *saml.EntityDescriptor object
idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient, *idpMetadataURL)
if err != nil {
// handle error however you want
}
// Get and decode public certificate from env
certPEM, err := base64.StdEncoding.DecodeString(os.Getenv("SAML_CERT"))
if err != nil {
// handle error however you want
}
// Get ande decode private key from env
keyPEM, err := base64.StdEncoding.DecodeString(os.Getenv("SAML_KEY"))
if err != nil {
// handle error however you want
}
// Create TLS certificate (object of type tls.Certificate)
tlsCertificate, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
// handle error however you want
}
// Go does not always parse the certificate automatically when creating a tls.Certificate.
// We parse it manually and populate the Leaf field because crewjam/saml reads certificate
// details (public key, validity, algorithms) from this field.
if tlsCertificate.Leaf == nil {
parsedCertificate, err := x509.ParseCertificate(tlsCertificate.Certificate[0])
if err != nil {
// handle error however you want
}
tlsCertificate.Leaf = parsedCertificate
}
// Verify that the private key implements crypto.Signer.
// This is required to sign SAML requests and assertions.
signer, ok := tlsCertificate.PrivateKey.(crypto.Signer)
if !ok {
// handle error however you want
}
sp, err := samlsp.New(samlsp.Options{
EntityID: os.Getenv("ENTRA_APPLICATION_ID"),
IDPMetadata: idpMetadata,
Key: signer,
Certificate: tlsCertificate.Leaf,
})
.
.
.
.
}
After we’ve prepared the certificate on our side and enabled request signing, there’s one more step left: we need to let Entra know which certificate we use to sign our SAML requests. Otherwise, Entra won’t be able to validate the signature and such requests will be rejected.
The way to do this is to send the public certificate to the IT admin, who will then manually upload it in the application’s SSO settings inside the Azure interface. It looks roughly like this:
If you don’t use a certificate to sign the AuthnRequest, Entra needs some other way to identify which application is sending the authentication request. In that scenario, identification comes down to comparing the Assertion Consumer Service URL — the one configured in Entra’s SSO settings and the one configured in your application via the AcsURL field on your ServiceProvider. That’s exactly why it’s necessary to set this field in code.
In other words: if the request isn’t signed, Entra doesn’t cryptographically verify the identity of the Service Provider — it relies solely on the fact that it will only send the response to a pre-registered ACS URL. If the URLs don’t match, the SAML response will be rejected.
This is explicitly stated in the official Microsoft Entra documentation:
A Signature element in AuthnRequest elements is optional. Microsoft Entra ID can be configured to enforce the requirement of signed authentication requests. If enabled, only signed authentication requests are accepted, otherwise the requestor verification is provided for by only responding to registered Assertion Consumer Service URLs.
In practice, this means the following:
If request signing is not required, the ACS URL becomes the only identification mechanism for your application.
Here’s a simple code example showing how to set it in the ServiceProvider configuration:
package controllers
import (
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
"github.com/labstack/echo/v4"
"net/http"
"net/url"
)
type AuthController struct {
ServiceProvider *samlsp.Middleware
}
func NewAuthController(ctx context.Context) *AuthController {
// It's is important that this string doesn't contain quotes
cleanIdpMetadataURL := os.Getenv("ENTRA_APP_FEDERATION_METADATA_URL")
cleanIdpMetadataURL = strings.Trim(cleanIdpMetadataURL, `"'`)
idpMetadataURL, err := url.Parse(cleanIdpMetadataURL)
if err != nil {
// handle error however you want
}
// Load IdP metadata, create *saml.EntityDescriptor object
idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient, *idpMetadataURL)
if err != nil {
// handle error however you want
}
sp, err := samlsp.New(samlsp.Options{
EntityID: os.Getenv("ENTRA_APPLICATION_ID"),
IDPMetadata: idpMetadata,
})
baseUrl, err := url.Parse(os.Getenv("BASE_URL_OF_YOUR_APP"))
if err != nil {
// handle error however you want
}
// This needs to be exactly the same as url you have in SSO settings in Entra
acsUrl, err := url.Parse(baseUrl.String() + "your/callback/endpoint")
if err != nil {
// handle error however you want
}
sp.ServiceProvider.AcsURL = *acsUrl
.
.
.
.
}
It’s important to emphasize this: the AcsURL value in your code must match the Reply URL configured in Entra 1:1. This isn’t a recommendation — it’s a strict requirement. Any mismatch will result in a failed SAML flow.
Before we jump into implementing the login itself, it’s worth explaining one more useful boolean attribute on the ServiceProvider — AllowIDPInitiated.
When this attribute is set to true, it enables IdP-initiated login, meaning the user can sign into the application directly from the Entra interface. In that case, the user doesn’t need to open your application first and click an “Entra login” button. Instead, they can simply click your application link inside the Entra portal and they’ll be automatically authenticated and let into the app.
In other words, our B. Simon can enter the application straight from the Entra dashboard, without any additional interaction with the app — Entra takes the initiative and sends the SAML response, and your application just validates it and accepts it.
sp, err := samlsp.New(samlsp.Options{
// other already mentione attributes
AllowIDPInitiated: true
// other already mentione attributes
})
Set this attribute according to your business rules, and keep in mind that its default value is false.
And finally — this is the part for all the “get shit done” developers, as well as for you who patiently made it all the way to the end.
Now is the moment when theory turns into practice.
It’s time for some copy–paste action.
Just keep in mind that you should copy the version of the code that matches your setup — depending on whether requests sent to Entra need to be signed or not (i.e., whether you need a signing certificate or not).
package controllers
import (
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
"github.com/labstack/echo/v4"
"net/http"
"net/url"
)
type AuthController struct {
ServiceProvider *samlsp.Middleware
}
func NewAuthController(ctx context.Context) *AuthController {
// It's is important that this string doesn't contain quotes
cleanIdpMetadataURL := os.Getenv("ENTRA_APP_FEDERATION_METADATA_URL")
cleanIdpMetadataURL = strings.Trim(cleanIdpMetadataURL, `"'`)
idpMetadataURL, err := url.Parse(cleanIdpMetadataURL)
if err != nil {
// handle error however you want
}
// Load IdP metadata, create *saml.EntityDescriptor object
idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient, *idpMetadataURL)
if err != nil {
// handle error however you want
}
// Get and decode public certificate from env
certPEM, err := base64.StdEncoding.DecodeString(os.Getenv("SAML_CERT"))
if err != nil {
// handle error however you want
}
// Get ande decode private key from env
keyPEM, err := base64.StdEncoding.DecodeString(os.Getenv("SAML_KEY"))
if err != nil {
// handle error however you want
}
// Create TLS certificate (object of type tls.Certificate)
tlsCertificate, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
// handle error however you want
}
// Go does not always parse the certificate automatically when creating a tls.Certificate.
// We parse it manually and populate the Leaf field because crewjam/saml reads certificate
// details (public key, validity, algorithms) from this field.
if tlsCertificate.Leaf == nil {
parsedCertificate, err := x509.ParseCertificate(tlsCertificate.Certificate[0])
if err != nil {
// handle error however you want
}
tlsCertificate.Leaf = parsedCertificate
}
// Verify that the private key implements crypto.Signer.
// This is required to sign SAML requests and assertions.
signer, ok := tlsCertificate.PrivateKey.(crypto.Signer)
if !ok {
// handle error however you want
}
sp, err := samlsp.New(samlsp.Options{
EntityID: os.Getenv("ENTRA_APPLICATION_ID"),
IDPMetadata: idpMetadata,
Key: signer,
Certificate: tlsCertificate.Leaf,
AllowIDPInitiated: false
})
return &AuthController{
ServiceProvider: sp,
}
}
package controllers
import (
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
"github.com/labstack/echo/v4"
"net/http"
"net/url"
)
type AuthController struct {
ServiceProvider *samlsp.Middleware
}
func NewAuthController(ctx context.Context) *AuthController {
// It's is important that this string doesn't contain quotes
cleanIdpMetadataURL := os.Getenv("ENTRA_APP_FEDERATION_METADATA_URL")
cleanIdpMetadataURL = strings.Trim(cleanIdpMetadataURL, `"'`)
idpMetadataURL, err := url.Parse(cleanIdpMetadataURL)
if err != nil {
// handle error however you want
}
// Load IdP metadata, create *saml.EntityDescriptor object
idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient, *idpMetadataURL)
if err != nil {
// handle error however you want
}
sp, err := samlsp.New(samlsp.Options{
EntityID: os.Getenv("ENTRA_APPLICATION_ID"),
IDPMetadata: idpMetadata,
AllowIDPInitiated: false
})
baseUrl, err := url.Parse(os.Getenv("BASE_URL_OF_YOUR_APP"))
if err != nil {
// handle error however you want
}
// This needs to be exactly the same as url you have in SSO settings in Entra
acsUrl, err := url.Parse(baseUrl.String() + "your/callback/endpoint")
if err != nil {
// handle error however you want
}
sp.ServiceProvider.AcsURL = *acsUrl
return &AuthController{
ServiceProvider: sp,
}
}
Alright, now that we have a properly configured Service Provider, we can finally use it for what we built it for in the first place — starting the login process and handling the SAML response that comes back from Entra.
AuthRequest
To even start the login flow, we first need to send an AuthnRequest to Entra. Using the crewjam/saml package, there are multiple ways to send that request — and the difference between them isn’t whether login works or not, but the level of abstraction you choose.
In other words, SAML login is not “one size fits all.” There are certain configurable parameters that give Entra additional context. Those parameters aren’t there to make SAML complicated — they exist to cover real-world scenarios applications run into, ranging from the simplest setup to a full enterprise-grade configuration.
Questions like where did the login start, where should the user be sent after authentication, how is the request physically delivered, or who controls the redirect might sound like minor details at first — but they’re exactly the kind of details that decide whether your login flow stays simple or becomes “enterprise-grade.” The parameters are:
- Binding — defines how the AuthnRequest is physically sent to Entra: via HTTP redirect or via an HTML form submit (POST). In most cases a redirect is enough and the library chooses it automatically, but in some enterprise environments you need control over that choice.
- RelayState — a small piece of context that travels with the AuthnRequest and is returned together with the SAML Response. It’s used when you want to know where the login started and where the user should be returned after successful authentication.
- Entry point — the place in your application where login can be initiated. If you only have a single “Login” button, you have one entry point. If login can start from multiple URLs, invite links, or actions, you have multiple entry points — and then your login flow needs to distinguish between those scenarios.
- Redirect control model — whether the backend directly returns an HTTP redirect to Entra, or whether it only generates a redirect URL and lets the frontend decide when and how the user gets redirected.
In the simplest setup, most of this doesn’t matter at all. You don’t think about RelayState, you don’t pick a binding, and you don’t care where the login started — you have one login button, one return URL, and you just want the user to successfully sign in. But as the application grows, situations appear where that additional context becomes important: multiple entry points, deep linking, different login flows, or specific security requirements. That’s exactly when the same AuthnRequest needs to carry more information, and when you need more control over how it’s sent.
In the next section, we’ll go through several ways of sending an AuthnRequest, explain what each of them does, what capabilities it gives you, and in which scenario it makes sense to use that approach. Each method will be backed by a practical code example — but the idea isn’t for you to blindly copy-paste a solution.
Don’t be a copy-paste zombie.
Read it, think about what you actually need, pick the right piece of code, and connect the dots in your head about what’s happening in the background — at least enough to understand the bigger picture. That’s the difference between a developer who only wants it to “work” and one who knows why it works.
Minimum AuthnRequest (middle level of abstraction)
Let’s start with the simplest possible case.
Imagine an application with a single login button. The user clicks “Login with Entra”, goes to the Entra login page, comes back, and you let them into the dashboard. No extra entry points, no deep-linking scenarios, and no need to remember any context between leaving for Entra and returning back.
In that case:
- the AuthnRequest exists, but you don’t care about what’s inside it
- you don’t care about the binding — Redirect is more than enough
- you don’t need RelayState, because there’s nothing you need to “remember”
This is the most common scenario for SPA applications.
func(a *AuthController) EntraLogin(c echo.Context) error {
redirectURL, err := a.ServiceProvider.ServiceProvider.MakeRedirectAuthenticationRequest("")
if err !=nil {
// handle error however you want
}
// Frontend will handle the actual redirect
return c.JSON(http.StatusOK, echo.Map{
"redirectUrl": redirectURL.String(),
})
}
Here’s what happens in this approach:
-
crewjam/samlgenerates an AuthnRequest - it uses the HTTP Redirect binding (implicitly)
-
it does not use RelayState — the
MakeRedirectAuthenticationRequestmethod accepts arelayStateparameter, but we pass an empty string, andcrewjam/samldoes not add RelayState to the SAML response in the backgroundRelayState Disclaimer
Even though
MakeRedirectAuthenticationRequestacceptsrelayStateas a parameter, it’s important to understand that this is a raw RelayState — a plain string that the library does not store anywhere, does not validate, and does not link to a specific login attempt. The string simply gets appended to the redirect to the identity provider, and Entra, after successful authentication, sends it back exactly as it received it.At first glance this sounds convenient, but in practice it introduces a few serious problems.
First, there is no server-side context. When the SAML Response comes back to your application, you have no reliable way of knowing which AuthnRequest that response belongs to. If the user opens multiple tabs, initiates login from different parts of the app, or has parallel login attempts, RelayState doesn’t help you separate those cases. Everything looks the same — and that’s a recipe for subtle and very painful bugs.
Second, RelayState comes from the client. That means the user or anyone with the will and a bit of knowledge can modify it. If you put data like a return URL into RelayState and later trust that string, you’ve very easily opened the door to open redirect attacks, bypassing authorization rules, or completely unexpected application behavior.
Third, the SAML specification limits RelayState to a very small size (roughly 80 bytes). That means you can’t really store meaningful context in it in a safe way without hacks, shortening, or encoding tricks that make things more complicated and increase the chance of mistakes.
In short: this approach can work in the simplest possible scenario — one login button, one entry point, no return context, and minimal security expectations. The moment you need anything beyond that, raw RelayState becomes a burden, not a solution.
That’s why
crewjam/samlprovides aRequestTracker. It uses RelayState correctly — not as a place to store data, but as a secure reference to server-side state. The real context is stored on the server, tied to the AuthnRequest ID, cryptographically protected, and validated when the SAML Response returns. Only then can you be sure the user is being returned to the right place, from a login flow that truly belongs to that user.If you want your login flow to be reliable, secure, and ready for the real enterprise world, raw RelayState simply isn’t the right tool for the job.
you receive a ready-made redirect URL
In this example, concepts like binding and RelayState exist only “under the hood” — it’s good to know they exist, but realistically they don’t affect your implementation.
If you recognize yourself in this scenario, you can safely stop here and ignore the rest of this section.
Multiple entry points and returning to the right place (high level of abstraction)
As an application grows, the following problem often shows up: login no longer always starts from the same place. A user might land directly on /reports/2024, click an invite link, or try to open a resource that requires authentication.
At that point:
- login has multiple entry points
- after a successful login, you want to send the user exactly back to where they started
- you need some way to carry context through the SAML roundtrip
That’s where RelayState becomes crucial.
Since RelayState is both size-limited and security-sensitive, crewjam/saml provides a ready-made mechanism for using it safely via the RequestTracker.
The simplest way to get all of that without manually messing around is to use HandleStartAuthFlow.
func(a *AuthController) EntraLogin(c echo.Context) error {
a.ServiceProvider.HandleStartAuthFlow(c.Response().Writer, c.Request())
return
}
Here, the level of abstraction increases:
- an AuthnRequest is still generated
- the binding is chosen automatically (Redirect → POST fallback)
- RelayState is used for tracking
- the backend immediately returns an HTTP redirect
The difference compared to the first level isn’t that login is somehow “more correct” — it’s that now you have context. If your application has multiple entry points or needs deep linking, this is the natural next step.
Full control over the AuthnRequest (lowest level of abstraction)
In some situations, neither of the previous approaches is sufficient. These are usually enterprise scenarios where:
- you want to manually choose the binding
- you need to use HTTP POST instead of Redirect
- you want full control over the contents of the AuthnRequest
- you are debugging issues with networks, proxies, or legacy browsers
In that case, you use the lowest level of the API and assemble the flow yourself.
func(a *AuthController) EntraLoginManual(c echo.Context)error {
binding := saml.HTTPRedirectBinding
bindingLocation := sp.GetSSOBindingLocation(binding)
authReq, err := sp.MakeAuthenticationRequest(bindingLocation, binding, a.ServiceProvider.ResponseBinding)
if err !=nil {
// handle error however you want
}
relayState :=""
redirectURL, err := authReq.Redirect(relayState, &a.ServiceProvider.ServiceProvider)
if err !=nil {
// handle error however you want
}
return c.JSON(http.StatusOK, echo.Map{
"redirectUrl": redirectURL.String(),
})
}
Here, all the concepts we mentioned earlier are completely in your hands:
- you choose the binding yourself
- you decide whether you want to use RelayState
- you control how and when the redirect happens
This level isn’t something most applications need, but it’s important to know it exists — because when you get stuck in a real enterprise environment, it’s often the only approach that gives you enough flexibility.
Binding defines the mechanics of how the message is transported (redirect vs. POST), while GetSSOBindingLocation uses that binding to find the exact Entra endpoint the AuthnRequest must be sent to (based on the IdP metadata). ResponseBinding, on the other hand, defines how we expect the SAML Response to return to our ACS URL — in practice, almost always via HTTP POST.
As for RelayState, it’s important to stress that it’s just a plain string with no protection and no server-side context. The library simply forwards it to the identity provider and expects it to come back unchanged. That means security, validation, and mapping the response back to the original login attempt are entirely the application’s responsibility. If you need reliable return context and resistance to manipulation, RelayState needs to be managed through mechanisms like RequestTracker, not manually.
func(a *AuthController) EntraLoginManual(c echo.Context)error {
.
.
// bindings
.
.
authReq, err := sp.MakeAuthenticationRequest(bindingLocation, binding, a.ServiceProvider.ResponseBinding)
if err !=nil {
// handle error however you want
}
relayState, err := a.ServiceProvider.RequestTracker.TrackRequest(
c.Response().Writer,
c.Request(),
authReq.ID,
)
if err != nil {
// handle error
}
.
.
// rest of the handler
.
.
.
}
SAML login isn’t “one flow” — it’s a spectrum of possibilities. crewjam/saml doesn’t force complexity on you; it allows you to add it gradually as new requirements show up. If you know what an AuthnRequest, binding, and RelayState are, and you understand when they start to matter, it becomes much easier to choose the right approach and to know when it’s time to move up to the next level.
Next, we’ll focus on the return trip from Entra — processing the SAML Response and mapping the authenticated user into our system.
Handling of SAML Response
We’ve reached the final, but by no means less important, step in the entire Entra login flow. This is exactly where you end up with two possible approaches, depending on how much control you want over what happens after successful authentication.
The first option is to completely hand things over to the crewjam/saml package to handle the SAML Response — meaning you simply set the ServiceProvider object itself as the handler for your ACS route. That approach looks like this:
authGroup.GET("/entra/login", centralController.AuthController.EntraLogin)
authGroup.POST("/entra/callback", echo.WrapHandler(centralController.AuthController.ServiceProvider))
This is the simplest possible setup — and to be clear right away, it’s perfectly valid. The package will automatically:
- validate the SAML Response
- create a session
- perform a redirect after successful authentication (to the RelayState, if it exists)
If your goal is simply “the user can log in and that’s it,” this is more than enough.
However, the price of that simplicity is a complete loss of control over the flow. With this approach, you have no opportunity to inject your own logic between validating the SAML Response and finishing the login process. Which means you can’t do things that, in the real world, you’ll very quickly end up needing.
For example, it’s very common to want to:
- store the user in your own database
- sync their email address, first name, and last name
- map Entra groups to application roles
- perform some kind of user provisioning or auditing
Even though you’re not storing the user’s login credentials yourself (which is the whole point of federated authentication), in practice you almost always want to keep at least some basic user information inside your system. That kind of “database sync” isn’t possible with this approach, because ServiceProvider completes the entire flow on its own, without any hook for your custom logic.
Luckily, there’s a second approach that gives you exactly what’s missing here — control. It looks roughly like this:
func (a *AuthController) EntraLoginCallbackDebug(c echo.Context) error {
req := c.Request()
// Validate SAML response
assertion, err := a.ServiceProvider.ServiceProvider.ParseResponse(req, []string{""})
if err != nil {
// handle error
}
// Create session and redirect to RelayState
a.ServiceProvider.CreateSessionFromAssertion(c.Response().Writer, req, assertion, "")
}
In this approach, we manually call the crewjam/saml engine and validate the SAML Response ourselves using the ParseResponse method. As a result, we get a *saml.Assertion object, which we’ll simply call assertion in this example. That object contains all the data about the authenticated user that Entra sends us.
We can then use that assertion to create a session. By “creating a session” here, I specifically mean calling the CreateSessionFromAssertion method, which under the hood:
- creates a session object
- sets the session cookie in the response
- enables that cookie to be validated later
It’s important to note that CreateSessionFromAssertion will automatically perform a redirect to the RelayState, so if you want to run additional logic inside your handler, you should either call CreateSessionFromAssertion at the very end or set the cookie manually and then handle the redirect manually as well.
Once the session is created, subsequent requests will automatically include that cookie, and its existence and validity are checked by the middleware that crewjam/saml provides — specifically, the RequireAccount middleware.
Below is an example of what that entire flow looks like in practice:
authGroup.POST("/super-protected-endpoint", CustomHandler, echo.WrapMiddleware(centralController.AuthController.ServiceProvider.RequireAccount))
As I mentioned earlier, the main advantage of this approach is that it lets us perform additional actions alongside SAML Response validation — for example, synchronizing the user with our own database. Even though we fully delegate authentication to Entra, in practice we almost always want to keep at least some basic user information inside our own system.
We get that data from the claims contained in the SAML Response. It’s important to emphasize that which claims are sent — and under which keys — is defined entirely in the Entra SSO settings. There, we can decide whether we want to send email, first name, last name, groups, roles, and similar data — and also exactly what claim names those values will appear under inside the assertion.
Also, we can set our own cookie.
https://learn.microsoft.com/en-us/entra/external-id/customers/how-to-add-attributes-to-token
func (a *AuthController) EntraLoginCallbackDebug(c echo.Context) error {
req := c.Request()
// Parse the form
err := req.ParseForm()
if err != nil {
// handle error
}
// Validate SAML response
assertion, err := a.ServiceProvider.ServiceProvider.ParseResponse(req, []string{""})
if err != nil {
// handle error
}
email := utils.GetEntraClaimAttribute(assertion, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")
firstName := utils.GetEntraClaimAttribute(assertion, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname")
lastName := utils.GetEntraClaimAttribute(assertion, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname")
ctx := c.Request().Context()
user, err := a.UserService.GetUserByEmail(ctx, email, false)
if errors.Is(err, gorm.ErrRecordNotFound) {
user, err = a.UserService.CreateUser(ctx, &models.User{
Email: email,
FirstName: firstName,
LastName: lastName,
})
if err != nil {
// handle error
}
}
jwtExpirationTime := time.Now().Add(24 * time.Hour)
jwtToken, err := a.AuthService.GenerateJwtToken(jwtExpirationTime, email, user.Uuid)
if err != nil {
// handle error
}
c.SetCookie(&http.Cookie{
Name: "jwt",
Value: jwtToken,
// Cookie after it expires, won't be sent anymore by browser
Expires: jwtExpirationTime,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteNoneMode,
Secure: true,
})
// Create session and redirect to RelayState
a.ServiceProvider.CreateSessionFromAssertion(c.Response().Writer, req, assertion, "")
// Or do your own redirect
// relayState := req.PostFormValue("RelayState")
// return c.Redirect(http.StatusTemporaryRedirect, relayState)
}
And with that, the entire login flow using Entra federated authentication is complete.
CONCLUSION
This is my first article and I honestly hope it helps you with your implementation in the same way it would have helped me if something like this had existed when I needed it. If along the way I overwhelmed you with unnecessary details and deep dives, sorry — I’m simply the type of person who likes to understand a problem all the way down to its roots. I wrote this article the way I would have liked someone to write it for me. Despite that, I made a conscious effort to clearly point out which parts are truly important and which ones are more “nice to know.”
And, just as a side note, I hope AI models will eventually stop hallucinating on this topic and that this article will one day end up in their “knowledge,” because there is clearly a lack of solid, practical material out there.
My goal was to save you time and make the implementation of this kind of login as painless as possible. But beyond that, I also wanted to spark at least a small desire for a bit more understanding. I firmly believe that understanding a problem from multiple perspectives is crucial if you want to build the right solution and that this is exactly what separates average developers from good ones.
I used AI exclusively for text editing, and I think you should use AI in the same way: as a tool that increases efficiency, not as a source of ultimate truth. Let’s not blindly trust everything AI serves us. During this implementation, I personally saw just how capable it is of hallucinating and copy–paste is the fastest way for AI hallucinations to become our own.
Let’s not hallucinate.
Let’s research.
Let’s learn.
Let’s be confident in ourselves.
Let’s be better than AI models — it’s really not that hard.











Top comments (0)