Skip to content

Add Microsoft OAuth provider support#304

Closed
need4swede wants to merge 1 commit intotinyauthapp:mainfrom
need4swede:microsoft-auth
Closed

Add Microsoft OAuth provider support#304
need4swede wants to merge 1 commit intotinyauthapp:mainfrom
need4swede:microsoft-auth

Conversation

@need4swede
Copy link
Copy Markdown

@need4swede need4swede commented Aug 8, 2025

Introduces Microsoft as a supported OAuth provider across backend and frontend. Adds Microsoft-specific config options, provider logic, user info retrieval, and UI icon/button for login. Updates configuration, provider initialization, and provider selection logic to include Microsoft.

Summary by CodeRabbit

  • New Features

    • Added support for Microsoft OAuth authentication, allowing users to log in with their Microsoft accounts.
    • Introduced a Microsoft login button on the login page with a recognizable Microsoft icon.
  • Chores

    • Updated configuration options and environment variables to support Microsoft OAuth integration.

Introduces Microsoft as a supported OAuth provider across backend and frontend. Adds Microsoft-specific config options, provider logic, user info retrieval, and UI icon/button for login. Updates configuration, provider initialization, and provider selection logic to include Microsoft.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 8, 2025

Walkthrough

Microsoft OAuth authentication support was integrated throughout the application. This includes new environment variables, configuration fields, command-line flags, provider logic, user info retrieval, and frontend login UI updates. The Microsoft provider is now treated as a first-class OAuth option, alongside GitHub, Google, and Generic providers.

Changes

Cohort / File(s) Change Summary
Environment Variables
.env.example
Added Microsoft OAuth-related environment variables: MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET, MICROSOFT_CLIENT_SECRET_FILE, MICROSOFT_AUTH_URL, MICROSOFT_TOKEN_URL, MICROSOFT_USER_URL, MICROSOFT_SCOPES.
CLI and Config Integration
cmd/root.go, internal/types/config.go
Added Microsoft OAuth fields to config structs and command-line flags. Updated environment variable bindings and validation to support Microsoft as an OAuth provider.
Provider Implementation
internal/providers/microsoft.go, internal/providers/providers.go
Added Microsoft provider implementation: user info retrieval, OAuth provider initialization, and integration into provider selection and user-fetching logic.
OAuth Configuration Utility
internal/utils/utils.go
Updated OAuth configuration check to include Microsoft credentials as valid for OAuth enablement.
Frontend: Microsoft Icon and Login
frontend/src/components/icons/microsoft.tsx, frontend/src/pages/login-page.tsx
Added a Microsoft logo icon component and updated the login page to display a Microsoft OAuth login button when configured.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant Backend
    participant MicrosoftOAuth

    User->>Frontend: Click "Sign in with Microsoft"
    Frontend->>Backend: Initiate OAuth ("microsoft" provider)
    Backend->>MicrosoftOAuth: Redirect to Microsoft Auth URL
    User->>MicrosoftOAuth: Authenticate & authorize
    MicrosoftOAuth->>Backend: Redirect with auth code
    Backend->>MicrosoftOAuth: Exchange code for token
    Backend->>MicrosoftOAuth: Fetch user info (/me endpoint)
    MicrosoftOAuth-->>Backend: Return user info (email, name)
    Backend-->>Frontend: Complete login (issue session/token)
    Frontend-->>User: User logged in as Microsoft account
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A hop and a skip, now Microsoft’s here,
OAuth’s embrace brings new logins near.
With icons that sparkle in red, green, and blue,
The login page welcomes a bunny or two.
Configs expanded, the code’s feeling bright—
More ways to sign in, by day or by night!
🐰✨

Note

🔌 MCP (Model Context Protocol) integration is now available in Early Access!

Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (9)
.env.example (1)

15-21: Microsoft OAuth env vars added — consider sample defaults and linter alignment

If desired, I can add commented examples and/or reorder keys per team convention.

frontend/src/components/icons/microsoft.tsx (1)

4-15: Optional: add basic a11y attributes

For decorative usage, default to non-focusable and hidden from AT; consumers can override via props.

   <svg
     width={24}
     height={24}
     viewBox="0 0 24 24"
     fill="none"
     xmlns="http://www.w3.org/2000/svg"
+    aria-hidden="true"
+    focusable="false"
     {...props}
   >
internal/utils/utils.go (1)

168-168: Consider secret-file as valid configuration for provider checks

Currently, a provider is considered “configured” only if client secret is inlined. If teams use the secret file option, the provider won’t be recognized as configured. Extend the check to include secret file presence.

- return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.MicrosoftClientId != "" && config.MicrosoftClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "")
+ return (config.GithubClientId != "" && (config.GithubClientSecret != "" || config.GithubClientSecretFile != "")) ||
+        (config.GoogleClientId != "" && (config.GoogleClientSecret != "" || config.GoogleClientSecretFile != "")) ||
+        (config.MicrosoftClientId != "" && (config.MicrosoftClientSecret != "" || config.MicrosoftClientSecretFile != "")) ||
+        (config.GenericClientId != "" && (config.GenericClientSecret != "" || config.GenericClientSecretFile != ""))

If you prefer readability, I can refactor to a small helper like isProviderConfigured(id, secret, secretFile).

frontend/src/pages/login-page.tsx (1)

134-143: Optional: reduce duplication across provider buttons

The four provider blocks are structurally identical. Consider a small map of provider → {title, icon} and iterate over configuredProviders to render, which will also make adding future providers trivial.

internal/providers/microsoft.go (1)

3-10: Group imports into std / third-party / internal blocks

Go convention (enforced by goimports / gci) is:

import (
	// std-lib

	// third-party

	// internal
)

Mixing tinyauth/internal/... with std-lib symbols in the same block will trigger linter warnings.

cmd/root.go (3)

73-92: Potential empty-slice sentinel – trim and filter scopes

strings.Split(config.MicrosoftScopes, ",") returns []string{""} when the flag/env is unset.
Although providers.NewProviders checks for exactly that pattern, trimming white-space first avoids edge cases like " ".

-MicrosoftScopes:       strings.Split(config.MicrosoftScopes, ","),
+MicrosoftScopes:       utils.SplitAndTrim(config.MicrosoftScopes, ","),

(Helper already exists for Google/Github; if not, add one.)


203-209: Flag descriptions: mention defaults to help operators

You already allow overriding auth / token / user URLs. Clarify that leaving them blank falls back to Microsoft’s public endpoints so users know they can skip these flags in common cases.


221-221: Keep root help text in sync

oauth-auto-redirect flag description now lists “microsoft” – great.
The rootCmd.Long string (Lines 28-31) still says “Google, Github and any generic OAuth provider”. Update it in a follow-up commit.

internal/providers/providers.go (1)

64-68: Trim empty elements when deciding on custom scopes

When MICROSOFT_SCOPES="", after the earlier split we may get []string{""} plus whitespace variants. Replace the length check with a helper that trims empty strings:

scopes := utils.FilterEmpty(config.MicrosoftScopes)
if len(scopes) == 0 {
    scopes = MicrosoftScopes()
}

Prevents accidental [" "] slipping through to oauth2.Config.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 256f63a and 25044d4.

📒 Files selected for processing (8)
  • .env.example (1 hunks)
  • cmd/root.go (5 hunks)
  • frontend/src/components/icons/microsoft.tsx (1 hunks)
  • frontend/src/pages/login-page.tsx (2 hunks)
  • internal/providers/microsoft.go (1 hunks)
  • internal/providers/providers.go (2 hunks)
  • internal/types/config.go (3 hunks)
  • internal/utils/utils.go (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-04-30T15:00:45.323Z
Learnt from: steveiliop56
PR: steveiliop56/tinyauth#122
File: internal/providers/github.go:64-66
Timestamp: 2025-04-30T15:00:45.323Z
Learning: GitHub's API provides user profile information through the `/user` endpoint, including name and login (username). The `GithubScopes()` function needs to include the `user` or `read:user` scope (in addition to `user:email`) to access this data.

Applied to files:

  • internal/providers/microsoft.go
🧬 Code Graph Analysis (3)
internal/types/config.go (1)
internal/providers/microsoft.go (1)
  • MicrosoftScopes (20-22)
cmd/root.go (1)
internal/utils/utils.go (1)
  • GetSecret (91-106)
internal/providers/microsoft.go (3)
internal/constants/constants.go (1)
  • Claims (4-9)
internal/providers/google.go (1)
  • GetGoogleUser (24-56)
internal/providers/github.go (1)
  • GetGithubUser (30-102)
🪛 dotenv-linter (3.3.0)
.env.example

[warning] 15-15: [UnorderedKey] The MICROSOFT_CLIENT_ID key should go before the PORT key


[warning] 16-16: [UnorderedKey] The MICROSOFT_CLIENT_SECRET key should go before the PORT key


[warning] 17-17: [UnorderedKey] The MICROSOFT_CLIENT_SECRET_FILE key should go before the PORT key


[warning] 18-18: [UnorderedKey] The MICROSOFT_AUTH_URL key should go before the MICROSOFT_CLIENT_ID key


[warning] 19-19: [UnorderedKey] The MICROSOFT_TOKEN_URL key should go before the PORT key


[warning] 20-20: [UnorderedKey] The MICROSOFT_USER_URL key should go before the PORT key


[warning] 21-21: [UnorderedKey] The MICROSOFT_SCOPES key should go before the MICROSOFT_TOKEN_URL key

🔇 Additional comments (5)
frontend/src/components/icons/microsoft.tsx (1)

1-17: LGTM: Microsoft icon component is clean and matches existing icon patterns

No functional issues found. Exports and sizing look consistent with other icons.

internal/types/config.go (2)

19-25: Config: Microsoft fields added — looks consistent

  • Fields mirror Google/GitHub patterns and include endpoints and secret file.
  • “Scopes” as string here (vs []string in OAuthConfig below) matches the Generic approach.

37-37: Validation updated to include microsoft in OAuthAutoRedirect

Good addition. Matches frontend usage where auto-redirect may be set to "microsoft".

frontend/src/pages/login-page.tsx (2)

5-5: Import Microsoft icon — consistent with icon usage elsewhere

Import path and named export look correct.


134-143: LGTM: Microsoft OAuth button matches existing provider button semantics

  • Same loading/disabled logic and mutation key usage.
  • Title and icon are aligned with other providers.

Comment on lines +20 to +22
func MicrosoftScopes() []string {
return []string{"openid", "profile", "email", "User.Read"}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Missing offline_access in default scope list

To obtain refresh tokens from Microsoft you generally need offline_access. Consider:

-return []string{"openid", "profile", "email", "User.Read"}
+return []string{"openid", "profile", "email", "offline_access", "User.Read"}

Without it, users will be forced to re-authenticate once the access token expires.

🤖 Prompt for AI Agents
In internal/providers/microsoft.go around lines 20 to 22, the MicrosoftScopes
function is missing the "offline_access" scope, which is necessary to obtain
refresh tokens and avoid forcing users to re-authenticate after access token
expiration. Add "offline_access" to the returned slice of scopes to ensure
refresh tokens are granted.

Comment on lines +33 to +37
res, err := client.Get(url)
if err != nil {
return user, err
}
defer res.Body.Close()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle non-200 HTTP responses

client.Get() may return 4xx/5xx responses that still read fine but contain an error payload. Check res.StatusCode before reading the body:

-res, err := client.Get(url)
+res, err := client.Get(url)
 if err != nil {
     return user, err
 }
+if res.StatusCode != http.StatusOK {
+    return user, fmt.Errorf("microsoft graph returned %d", res.StatusCode)
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res, err := client.Get(url)
if err != nil {
return user, err
}
defer res.Body.Close()
res, err := client.Get(url)
if err != nil {
return user, err
}
if res.StatusCode != http.StatusOK {
return user, fmt.Errorf("microsoft graph returned %d", res.StatusCode)
}
defer res.Body.Close()
🤖 Prompt for AI Agents
In internal/providers/microsoft.go around lines 33 to 37, after calling
client.Get(url), check if res.StatusCode is not 200. If it is not 200, handle
the error appropriately by returning an error indicating the unexpected status
code before proceeding to read the response body. This ensures that non-200 HTTP
responses are properly handled and do not cause incorrect processing.

Comment on lines +57 to +64
// Prefer mail, fallback to UserPrincipalName
email := userInfo.Mail
if email == "" {
email = userInfo.UserPrincipalName
}
user.PreferredUsername = strings.Split(email, "@")[0]
user.Name = userInfo.DisplayName
user.Email = email
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Return an error if no email could be determined

mail and userPrincipalName can both be empty for some tenants, leading to an empty PreferredUsername and silent acceptance.

 email := userInfo.Mail
 if email == "" {
     email = userInfo.UserPrincipalName
 }
+if email == "" {
+    return user, errors.New("no email returned from Microsoft Graph")
+}

This aligns with the GitHub path where absence of email is considered an error.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Prefer mail, fallback to UserPrincipalName
email := userInfo.Mail
if email == "" {
email = userInfo.UserPrincipalName
}
user.PreferredUsername = strings.Split(email, "@")[0]
user.Name = userInfo.DisplayName
user.Email = email
// Prefer mail, fallback to UserPrincipalName
email := userInfo.Mail
if email == "" {
email = userInfo.UserPrincipalName
}
if email == "" {
return user, errors.New("no email returned from Microsoft Graph")
}
user.PreferredUsername = strings.Split(email, "@")[0]
user.Name = userInfo.DisplayName
user.Email = email
🤖 Prompt for AI Agents
In internal/providers/microsoft.go around lines 57 to 64, the code silently
accepts when both mail and UserPrincipalName are empty, resulting in an empty
PreferredUsername. Modify the code to check if both email sources are empty, and
if so, return an error indicating that no email could be determined. This
ensures consistent error handling aligned with the GitHub path and prevents
silent acceptance of incomplete user data.

Comment on lines +149 to +156
client := providers.Microsoft.GetClient()
log.Debug().Msg("Got client from microsoft")
user, err := GetMicrosoftUser(client, providers.Config.MicrosoftUserURL)
if err != nil {
return user, err
}
log.Debug().Msg("Got user from microsoft")
return user, nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Propagate custom user-info URL only when non-empty

Passing an empty string as a variadic argument yields []string{""} which overrides the default inside GetMicrosoftUser. Add a guard:

var userURL []string
if providers.Config.MicrosoftUserURL != "" {
    userURL = []string{providers.Config.MicrosoftUserURL}
}
user, err := GetMicrosoftUser(client, userURL...)

Prevents "" from becoming an invalid request URL.

🤖 Prompt for AI Agents
In internal/providers/providers.go around lines 149 to 156, the code passes
providers.Config.MicrosoftUserURL directly to GetMicrosoftUser, which causes an
empty string to override the default URL. Fix this by adding a check: only
create a slice with MicrosoftUserURL if it is non-empty, then pass that slice
variadically to GetMicrosoftUser. This prevents passing an empty string and
avoids invalid request URLs.

Comment thread internal/types/config.go
Comment on lines +71 to 89
GithubClientId string
GithubClientSecret string
GoogleClientId string
GoogleClientSecret string
MicrosoftClientId string
MicrosoftClientSecret string
MicrosoftAuthURL string
MicrosoftTokenURL string
MicrosoftUserURL string
MicrosoftScopes []string
GenericClientId string
GenericClientSecret string
GenericScopes []string
GenericAuthURL string
GenericTokenURL string
GenericUserURL string
GenericSkipSSL bool
AppURL string
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure Config → OAuthConfig mapping and scope parsing cover Microsoft

  • OAuthConfig adds Microsoft fields + []string scopes. Confirm the wiring that:
    • Splits Config.MicrosoftScopes (string) into []string for OAuthConfig.MicrosoftScopes (e.g., comma-separated).
    • Applies defaults for Auth/Token/User URLs if unset (common/tenant-specific as applicable).

Run to locate and verify the mapping logic and defaults:


🏁 Script executed:

#!/bin/bash
# 1) Where Config is transformed into OAuthConfig
rg -n "OAuthConfig\s*{"
rg -n "MicrosoftClientId|MicrosoftClientSecret|MicrosoftAuthURL|MicrosoftTokenURL|MicrosoftUserURL|MicrosoftScopes" internal

# 2) Where scopes are parsed from config (string -> []string), similar to GenericScopes
rg -n "MicrosoftScopes.*\[\]string|strings\.Split\(.+microsoft-scopes" 

# 3) Where default MS endpoints are set if empty (providers/microsoft.go or providers.go)
rg -n "microsoft.*(authorize|token|user|graph|login\.microsoftonline\.com|/v2\.0|/me)" internal

Length of output: 3162


🏁 Script executed:

#!/bin/bash
set -e
# Dump raw Config and OAuthConfig definitions to inspect field types and any mapping helpers
echo "=== internal/types/config.go:1-60 ==="
sed -n '1,60p' internal/types/config.go
echo
echo "=== internal/types/config.go:60-120 ==="
sed -n '60,120p' internal/types/config.go
# Search for any conversion helpers (methods that turn Config into OAuthConfig)
rg -n "func .*OAuthConfig" -n internal/types/config.go || echo "No direct converter found"
rg -n "Split" -n internal/types/config.go || echo "No strings.Split in config.go"

Length of output: 5614


🏁 Script executed:

#!/bin/bash
set -e
# Show the Microsoft provider setup in providers/providers.go
echo "=== internal/providers/providers.go (Lines 1-120) ==="
sed -n '1,120p' internal/providers/providers.go

echo
echo "=== internal/providers/providers.go (Lines 120-200) ==="
sed -n '120,200p' internal/providers/providers.go

# Show the MicrosoftScopes helper and default user URL in microsoft.go
echo
echo "=== internal/providers/microsoft.go (Entire file) ==="
sed -n '1,200p' internal/providers/microsoft.go

Length of output: 7606


Microsoft OAuth: Add scope parsing in config→OAuthConfig conversion

The providers layer already handles defaulting for auth, token, and user URLs (falls back to common v2.0 endpoints and Graph “/me” if unset) and will only invoke MicrosoftScopes() when the slice is empty. However, I did not find any code that converts the raw Config.MicrosoftScopes (a comma-separated string) into the []string used by OAuthConfig.MicrosoftScopes.

Please update your config-loading or main setup (where you build the types.OAuthConfig from types.Config) to:

  • Split Config.MicrosoftScopes on commas (e.g. strings.Split(raw, ","))
  • Trim whitespace from each element
  • Assign that slice to OAuthConfig.MicrosoftScopes

With that in place, custom scopes will be respected and the built-in fallback only kicks in when no scopes are provided.

🤖 Prompt for AI Agents
In internal/types/config.go around lines 71 to 89, the MicrosoftScopes field is
defined as a []string but the raw config likely provides it as a comma-separated
string. To fix this, locate the code where you convert the raw Config into
OAuthConfig and add logic to split the raw MicrosoftScopes string by commas,
trim whitespace from each resulting element, and assign the cleaned slice to
OAuthConfig.MicrosoftScopes. This ensures custom scopes are properly parsed and
used instead of relying solely on the default fallback.

@steveiliop56
Copy link
Copy Markdown
Member

Hello!

Is there any way to test this? Because I was originally planning to add Microsoft OAuth but I discovered I need Entra ID which is some business feature or something that's why I avoided Microsoft and let people manually configure it. Also I don't believe we need the custom URLs or scopes. Usually with built-in OAuth providers I use the defaults to ship an optimal config.

@need4swede
Copy link
Copy Markdown
Author

need4swede commented Aug 10, 2025

@steveiliop56 I tested it and it worked. Registered a new app in Entra, create a new auth token, and add these bad boys to the .env for Tinyauth

MICROSOFT_CLIENT_ID=<redacted>
MICROSOFT_CLIENT_SECRET=<redacted>
MICROSOFT_AUTH_URL=https://login.microsoftonline.com/<redacted>/oauth2/v2.0/authorize
MICROSOFT_TOKEN_URL=https://login.microsoftonline.com/<redacted>/oauth2/v2.0/token
MICROSOFT_USER_URL=https://graph.microsoft.com/v1.0/me
MICROSOFT_SCOPES=openid,profile,email,User.Read

And you're right, scopes are strictly needed - the URL's are needed for the authentication flow to work.

Here's a video of it working.
This is also using my custom-branding PR, hence the custom login text.
#306

Screen.Recording.2025-08-09.at.20.57.43.mp4

Logs:

2025-08-10T03:57:45Z DBG Got OAuth request
2025-08-10T03:57:45Z DBG Got provider provider=microsoft
2025-08-10T03:57:45Z DBG Got auth URL
2025-08-10T03:57:45Z INF Request address=192.168.65.1:39904 latency="322.75µs" method=GET path=/api/oauth/url/microsoft status=200
2025-08-10T03:57:53Z DBG Got provider name provider=microsoft
2025-08-10T03:57:53Z DBG Got CSRF cookie csrfCookie=<redacted>
2025-08-10T03:57:53Z DBG Got code
2025-08-10T03:57:53Z DBG Got provider provider=microsoft
2025-08-10T03:57:53Z DBG Got token
2025-08-10T03:57:53Z DBG Got client from microsoft
2025-08-10T03:57:53Z DBG Got response from microsoft
2025-08-10T03:57:53Z DBG Read body from microsoft
2025-08-10T03:57:53Z DBG Parsed user from microsoft
2025-08-10T03:57:53Z DBG Got user from microsoft
2025-08-10T03:57:53Z DBG Got user user={"email":"authenticator@njes.org","groups":null,"name":"Authenticator","preferred_username":"authenticator"}
2025-08-10T03:57:53Z DBG Email whitelisted

@steveiliop56
Copy link
Copy Markdown
Member

It's not about not believing you if it works or not, of course I do believe you lol. My issue is that in my testing "flow" I test every function manually and if cannot setup entra myself, I cannot test the Microsoft OAuth function. Is there any way to use entra myself without needing any business plans or similar?

@need4swede
Copy link
Copy Markdown
Author

Not that I know of. Pretty sure it's an enterprise feature.

@steveiliop56
Copy link
Copy Markdown
Member

Yeah that's the reason I avoided adding it. Does entra work with the generic OAuth?

@need4swede
Copy link
Copy Markdown
Author

I'll have to do some testing first to see if various apps work with generic Auth.
I wanted to add something more 'native', hence the PR.

@Fatiz
Copy link
Copy Markdown

Fatiz commented Aug 21, 2025

Entra does work with the generic provider!

My setup is:

- GENERIC_CLIENT_ID=<redacted>
- GENERIC_CLIENT_SECRET=<redacted>
- GENERIC_AUTH_URL=https://login.microsoftonline.com/<redacted>/oauth2/v2.0/authorize
- GENERIC_TOKEN_URL=https://login.microsoftonline.com/<redacted>/oauth2/v2.0/token
- GENERIC_USER_URL=https://graph.microsoft.com/oidc/userinfo
- GENERIC_NAME=azure-ad
- GENERIC_SCOPES=openid email profile
- OAUTH_AUTO_REDIRECT=generic
- LOG_LEVEL=0
- DISABLE_CONTINUE=true
  1. Note the usage of 'https://graph.microsoft.com/oidc/userinfo' rather than 'https://graph.microsoft.com/v1.0/me'
  2. It may require ID Tokens to be enabled on the application. (I have it enabled, but not sure if it would be required for it to function)

One big issue I was running into is that, for my use case, using the generic provider, it doesn't seem that there is any way to get the groups associated with users for use in access control.

Based on my understanding (and reading Microsoft's docs), it seems that if using the userinfo endpoint (https://graph.microsoft.com/oidc/userinfo), there is no way to get the group information. Instead, it is expected that applications validate the provided ID token/access token for the optional claims (such as the user's groups): https://learn.microsoft.com/en-us/entra/identity-platform/optional-claims

I don't believe Tinyauth does anything like this at all currently (Although feel free to correct me if I'm wrong). If it were to be done, I do think it would require a significant change to the whole OAuth flow since I don't think we'd have to care about the "USER_URL" anymore and could get all user information from the ID token itself.

Regarding the contents of the PR, I am not sure if there is also a means to get the users' "groups" provided to https://graph.microsoft.com/v1.0/me, but that would be very very nice... 🙏
(I dont think it provides them by default)

@need4swede
Copy link
Copy Markdown
Author

@Fatiz
Thank you for confirming! I had issues getting generic auth working with entra, too.
My plan is to wait for v4's release before adding more features, as per @steveiliop56
Afterwards, I can look into adding support for getting groups, etc.

@steveiliop56
Copy link
Copy Markdown
Member

Guys not sure if I should merge this because I cannot test any changes in the future. Not old enough to have a debit card so I can't sign up for the free version of azure. Although I will see what I can do.

@need4swede
Copy link
Copy Markdown
Author

@steveiliop56
I don't think you should merge anything you are not comfortable with. This is your repo - it's 100% up to you how you would like to proceed.

If anyone wants to use the features I built, they can always use my fork: https://github.com/need4swede/tinyauth
It's the two PR's I put in, plus updated documentation (in the repo). You can always revisit these things at a later time.

@thechiefn
Copy link
Copy Markdown

This can be accomplished via the generic OAuth provider, as well. I've implemented it that way, and the flow works fine in all use cases.

@steveiliop56, I would be happy to set you up with a tenant to test in if you are interested.

@steveiliop56
Copy link
Copy Markdown
Member

steveiliop56 commented Aug 26, 2025

Sure why not. By the way I made some tiny changes to the codebase so probably some refactoring will have to take place in the pull request ; )

@jdw1023
Copy link
Copy Markdown

jdw1023 commented Sep 17, 2025

Hi @Fatiz , I was testing your method of using the generic OAuth provider with Entra ID and discovered a potential security vulnerability when using the email whitelist (OAUTH_WHITELIST).
The issue is that Entra ID allows a user to change their account's email address property to any value without requiring verification. So an unauthorized user could gain access to the system by changing email property on Entra ID to email/domain that is on the whitelist.

image

I haven't tested the code in this pull request but looking at the code. I think it might suffer the same issue.

	log.Debug().Msg("Parsed user from microsoft")

	// Prefer mail, fallback to UserPrincipalName
	email := userInfo.Mail
	if email == "" {
		email = userInfo.UserPrincipalName
	}
	user.PreferredUsername = strings.Split(email, "@")[0]
	user.Name = userInfo.DisplayName
	user.Email = email

This behavior is documented by Microsoft here, confirming that email claims in Entra ID are unverified.

@steveiliop56
I'd like to suggest a few potential project-wide fix to enhance security:

  • Enforce Email Verification: all OAuth/OIDC flows could be updated to check for an email_verified: true claim in the token. If the claim is false or missing, the authentication should be rejected, even if the email is on the whitelist. Unfortunately not all OIDC flow contain the email_verified claim (for example microsoft entra id doesn't seem to have it https://learn.microsoft.com/en-us/entra/identity-platform/claims-validation#:~:text=Never%20use%20claims,unreliable%20for%20authorization.)
  • Add Documentation Warnings: Adding a warning to the documentation about the security risks of using email-based whitelists with certain providers would be very helpful for administrators setting up tinyauth.
    e.g.
image

This issue might exist in others if relying on the email claim. (I think google and github oauth should be safe but probably should still check the email_verified claim.)

@steveiliop56
Copy link
Copy Markdown
Member

Hello @jdw1023,

Thank you for letting me know. I believe this issue is specific to Microsoft OAuth so that's where security precautions must be taken. In any way the pull request in its current state cannot be merged due to the changes in the main codebase. @need4swede would you like to rework on it?

@need4swede
Copy link
Copy Markdown
Author

Yeah I think I'm going to wait for v4 before I submit any additional changes. Feel free to close the PR for now and I can revisit this addition with the new major release

@steveiliop56
Copy link
Copy Markdown
Member

Alright, closing this and #306. Feel free to reopen the pull requests after v4. Hopefully the changes in the codebase will make development much easier.

@steveiliop56 steveiliop56 mentioned this pull request Nov 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants