> ## Content Index
> Fetch the complete content index at: https://anantafatur.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Jenkins Azure AD login redirects to root instead of original URL
- URL: https://anantafatur.dev/jenkins-azure-ad-redirect-root/
- Published: 2026-08-22T19:00:52.000Z
- Updated: 2026-08-22T19:01:12.000Z
- Description: A walkthrough of debugging a Jenkins OAuth redirect issue where users landed on the root URL after Azure AD login instead of the original page they requested.
- Author: Ananta

We run Jenkins behind nginx, with Azure AD for authentication. A user reported that when they tried to access a deep URL, like `/job/<team>/<project>/<pipeline>/`, they got bounced to Azure AD to log in. So far, normal. But after logging in, they landed on the Jenkins root URL instead of the page they originally asked for. Every time.

This kind of thing is annoying. You click a link someone sent you, you authenticate, and then you have to navigate back to where you wanted to go. Multiply that by a team of engineers and it adds up fast.

## The setup

Jenkins is running in a Docker container behind an nginx reverse proxy, both on the same VM. The HTTPS certificate is terminated by a cloud load balancer, which forwards HTTP to nginx.

The DNS for `<jenkins-domain>` is a CNAME pointing to the load balancer's public DNS name. The load balancer's public IP is something like `<lb-public-ip>`.

Jenkins is configured with its URL set to `https://<jenkins-domain>/`. The Azure AD app registration has the Reply URL set to `https://<jenkins-domain>/securityRealm/finishLogin`. Both look correct.

So what was going wrong?

---

## First clue: the NullPointerException

The Jenkins logs had this error, repeated every time someone tried to log in:

```
Error while serving https://<lb-public-ip>/securityRealm/commenceLogin
java.lang.NullPointerException: Cannot invoke "Object.toString()" because the
return value of "jakarta.servlet.http.HttpSession.getAttribute(String)" is null
    at PluginClassLoader for azure-ad//com.microsoft.jenkins.azuread.oauth.StateCache.generateValue(StateCache.java:18)
    at PluginClassLoader for azure-ad//com.microsoft.jenkins.azuread.AzureSecurityRealm.doCommenceLogin(AzureSecurityRealm.java:388)

```

Two things jumped out at me.

First, the URL in the error message was `https://<lb-public-ip>/securityRealm/commenceLogin`. Not `<jenkins-domain>`. The IP address. That meant Jenkins was seeing the request as coming from the load balancer's IP, not the domain name.

Second, the `StateCache.generateValue` method was failing because a session attribute was null. The `StateCache` is what stores the original URL the user was trying to access. If it can't find the session attribute, the original URL is lost.

## Why the load balancer IP was showing up

The request flow is:

```
Browser (DNS) → CNAME → Load balancer → nginx → Jenkins

```

The load balancer terminates HTTPS and forwards HTTP to nginx. But when it forwards the request, it sends the `Host` header as its own IP address, not the original domain. My nginx config had:

```nginx
proxy_set_header Host $host;

```

The `$host` variable in nginx evaluates to whatever `Host` header it received from upstream. In this case, the load balancer was sending `Host: <lb-public-ip>`. So nginx forwarded that to Jenkins. Jenkins then used the IP address to construct its redirect URLs.

When Jenkins redirected the browser to `/securityRealm/commenceLogin`, the browser got a URL with the IP address. The browser went there, but the JSESSIONID cookie was set for the domain `<jenkins-domain>`. The browser didn't send the cookie to the IP address. Jenkins saw a brand new session with no saved attributes. The `StateCache` crashed.

## Fix 1: hardcode the Host header in nginx

The fix was simple. Instead of forwarding whatever `Host` header the load balancer sent, I hardcoded it to the domain:

```nginx
proxy_set_header Host <jenkins-domain>;
proxy_set_header X-Forwarded-Host <jenkins-domain>;

```

I also replaced `proxy_redirect off` with proper redirect rewriting, just as a safety net:

```nginx
proxy_redirect http://<jenkins-container>:8080/ https://<jenkins-domain>/;
proxy_redirect http://<jenkins-container>:8080 https://<jenkins-domain>/;

```

After restarting nginx, the `NullPointerException` was gone. The `doCommenceLogin` method was completing without errors. But the redirect after login was still going to the root URL.

## Second clue: the updateAvatar error

The new logs showed a different error:

```
WARNING ... AzureSecurityRealm updateAvatar
Failed to get profile photo for <user-oid>
com.microsoft.graph.http.GraphServiceException: 401 : Unauthorized
GET https://graph.microsoft.com/v1.0/users/<user-oid>/photos/48x48

```

The `doFinishLogin` method was now being called successfully. It authenticated the user. Then it called `updateAvatar`, which tried to fetch the user's profile photo from Microsoft Graph. That call failed with a 401.

I looked at the Azure AD app permissions. The app had `User.Read` (Delegated) for Microsoft Graph. But looking at the plugin source code, the `getAzureClient()` method uses client credentials (application context) to call the Graph API. The `getAccessToken()` method requests the `.default` scope, which only resolves application permissions. `User.Read` is a delegated permission, not an application permission. So the token didn't have the right scope.

But here's the thing. Looking at the `updateAvatar` method in the plugin source, it catches the `ApiException` and logs it. It doesn't re-throw it. So the `doFinishLogin` method should continue to the redirect code. Yet the redirect was still going to the root URL.

I was about to try disabling the avatar fetching with a system property when I noticed something in the plugin changelog.

## The real fix: plugin version 668

The Azure AD plugin we had installed was version **667**. Version **668** had a single fix listed:

> "Restore support for redirecting to non root path" (PR #800)

That was it. The redirect to the root URL was a known bug in version 667, fixed in 668\. The `updateAvatar` error was a red herring. It was logged as a warning, but the real reason the redirect was failing was the plugin bug.

Upgrading to version 668 fixed the issue immediately. After the upgrade, users who logged in through Azure AD were redirected to the original URL they requested, not the root.

---

## Closing

The nginx `Host` header fix was still necessary. The load balancer sending the IP as the `Host` header was causing the `StateCache` NullPointerException, which was a separate issue. If I had only upgraded the plugin, the login would still have failed with the first error.

The `updateAvatar` error was a distraction. It was logged as a warning but didn't actually block the redirect. I spent time looking at Graph API permissions when the real fix was a one-line plugin upgrade.

After all changes, here's what the nginx config looks like:

```nginx
server {
    listen 80;
    server_name <jenkins-domain>;

    location / {
        proxy_pass http://<jenkins-container>:8080;

        proxy_http_version 1.1;

        proxy_set_header Host <jenkins-domain>;
        proxy_set_header X-Forwarded-Host <jenkins-domain>;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Port 443;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_redirect http://<jenkins-container>:8080/ https://<jenkins-domain>/;
        proxy_redirect http://<jenkins-container>:8080 https://<jenkins-domain>/;
    }
}

```

And the Azure AD plugin is now on version 668.