Skip to content
This repository was archived by the owner on Oct 10, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
version: "2"
issues:
max-issues-per-linter: 0
max-same-issues: 0
linters:
default: all
settings:
Expand Down
1 change: 1 addition & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2507,6 +2507,7 @@ components:
- bitbucket
- workos
- azuread
- entraid
- strava
- facebook
- windowslive
Expand Down
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go/api/api.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package api

const IdTokenProviderFake = IdTokenProvider("fake") //nolint:revive,stylecheck
const IdTokenProviderFake = IdTokenProvider("fake") //nolint:revive
318 changes: 159 additions & 159 deletions go/api/server.gen.go

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions go/api/types.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions go/cmd/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"log/slog"

"github.com/nhost/hasura-auth/go/api"
"github.com/nhost/hasura-auth/go/providers"
Expand Down Expand Up @@ -33,6 +34,8 @@ func getDefaultScopes(providerName api.SignInProvider) []string {
return providers.DefaultWorkOSScopes
case api.SignInProviderAzuread:
return providers.DefaultAzureadScopes
case api.SignInProviderEntraid:
return providers.DefaultEntraIDScopes
case api.SignInProviderFacebook:
return providers.DefaultFacebookScopes
case api.SignInProviderWindowslive:
Expand Down Expand Up @@ -66,6 +69,7 @@ func getScopes(provider api.SignInProvider, scopes []string) []string {
//nolint:funlen,cyclop
func getOauth2Providers(
cCtx *cli.Context,
logger *slog.Logger,
) (providers.Map, error) {
providersMap := make(providers.Map)

Expand Down Expand Up @@ -180,6 +184,10 @@ func getOauth2Providers(
}

if cCtx.Bool(flagAzureadEnabled) {
logger.WarnContext(
cCtx.Context, "AzureAD provider is deprecated, use EntraID provider instead",
)

providersMap["azuread"] = providers.NewAzureadProvider(
cCtx.String(flagAzureadClientID),
cCtx.String(flagAzureadClientSecret),
Expand All @@ -189,6 +197,16 @@ func getOauth2Providers(
)
}

if cCtx.Bool(flagEntraIDEnabled) {
providersMap["entraid"] = providers.NewEntraIDProvider(
cCtx.String(flagEntraIDClientID),
cCtx.String(flagEntraIDClientSecret),
cCtx.String(flagServerURL),
cCtx.String(flagEntraIDTenant),
getScopes(api.SignInProviderEntraid, cCtx.StringSlice(flagEntraIDScope)),
)
}

if cCtx.Bool(flagFacebookEnabled) {
providersMap["facebook"] = providers.NewFacebookProvider(
cCtx.String(flagFacebookClientID),
Expand Down
42 changes: 41 additions & 1 deletion go/cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@ const (
flagAzureadClientSecret = "azuread-client-secret" //nolint:gosec
flagAzureadTenant = "azuread-tenant"
flagAzureadScope = "azuread-scope"
flagEntraIDEnabled = "entraid-enabled"
flagEntraIDClientID = "entraid-client-id"
flagEntraIDClientSecret = "entraid-client-secret" //nolint:gosec
flagEntraIDTenant = "entraid-tenant"
flagEntraIDScope = "entraid-scope"
flagFacebookEnabled = "facebook-enabled"
flagFacebookClientID = "facebook-client-id"
flagFacebookClientSecret = "facebook-client-secret"
Expand Down Expand Up @@ -1042,6 +1047,7 @@ func CommandServe() *cli.Command { //nolint:funlen,maintidx
Name: flagAzureadTenant,
Usage: "Azuread Tenant",
Category: "oauth-azuread",
Value: "common",
EnvVars: []string{"AUTH_PROVIDER_AZUREAD_TENANT"},
},
&cli.StringSliceFlag{ //nolint: exhaustruct
Expand All @@ -1051,6 +1057,40 @@ func CommandServe() *cli.Command { //nolint:funlen,maintidx
Value: cli.NewStringSlice(providers.DefaultAzureadScopes...),
EnvVars: []string{"AUTH_PROVIDER_AZUREAD_SCOPE"},
},
// Microsoft EntraID flags
&cli.BoolFlag{ //nolint: exhaustruct
Name: flagEntraIDEnabled,
Usage: "Enable EntraID OAuth provider",
Category: "oauth-entraid",
Value: false,
EnvVars: []string{"AUTH_PROVIDER_ENTRAID_ENABLED"},
},
&cli.StringFlag{ //nolint: exhaustruct
Name: flagEntraIDClientID,
Usage: "EntraID OAuth client ID",
Category: "oauth-entraid",
EnvVars: []string{"AUTH_PROVIDER_ENTRAID_CLIENT_ID"},
},
&cli.StringFlag{ //nolint: exhaustruct
Name: flagEntraIDClientSecret,
Usage: "EntraID OAuth client secret",
Category: "oauth-entraid",
EnvVars: []string{"AUTH_PROVIDER_ENTRAID_CLIENT_SECRET"},
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagEntraIDTenant,
Usage: "EntraID Tenant",
Category: "oauth-entraid",
Value: "common",
EnvVars: []string{"AUTH_PROVIDER_ENTRAID_TENANT"},
},
&cli.StringSliceFlag{ //nolint: exhaustruct
Name: flagEntraIDScope,
Usage: "EntraID OAuth scope",
Category: "oauth-entraid",
Value: cli.NewStringSlice(providers.DefaultEntraIDScopes...),
EnvVars: []string{"AUTH_PROVIDER_ENTRAID_SCOPE"},
},
// Facebook provider flags
&cli.BoolFlag{ //nolint: exhaustruct
Name: flagFacebookEnabled,
Expand Down Expand Up @@ -1271,7 +1311,7 @@ func getGoServer( //nolint:funlen
return nil, err
}

oauthProviders, err := getOauth2Providers(cCtx)
oauthProviders, err := getOauth2Providers(cCtx, logger)
if err != nil {
return nil, fmt.Errorf("problem creating oauth providers: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions go/controller/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,13 @@ func (response ErrorResponse) VisitRefreshTokenResponse(w http.ResponseWriter) e
return response.visit(w)
}

func (response ErrorResponse) VisitSignInIdTokenResponse( //nolint:revive,stylecheck
func (response ErrorResponse) VisitSignInIdTokenResponse( //nolint:revive
w http.ResponseWriter,
) error {
return response.visit(w)
}

func (response ErrorResponse) VisitLinkIdTokenResponse( //nolint:revive,stylecheck
func (response ErrorResponse) VisitLinkIdTokenResponse( //nolint:revive
w http.ResponseWriter,
) error {
return response.visit(w)
Expand Down
2 changes: 1 addition & 1 deletion go/controller/link_id_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/nhost/hasura-auth/go/middleware"
)

func (ctrl *Controller) LinkIdToken( //nolint:ireturn,revive,stylecheck
func (ctrl *Controller) LinkIdToken( //nolint:ireturn,revive
ctx context.Context, req api.LinkIdTokenRequestObject,
) (api.LinkIdTokenResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
Expand Down
2 changes: 1 addition & 1 deletion go/controller/sign_in_id_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (ctrl *Controller) postSigninIdtokenCheckUserExists(
return user, false, false, nil
}

func (ctrl *Controller) SignInIdToken( //nolint:ireturn,revive,stylecheck
func (ctrl *Controller) SignInIdToken( //nolint:ireturn,revive
ctx context.Context, req api.SignInIdTokenRequestObject,
) (api.SignInIdTokenResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
Expand Down
5 changes: 5 additions & 0 deletions go/controller/sign_in_provider_callback_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ func (ctrl *Controller) signinProviderProviderCallbackOauthFlow(
}
}

if profile.ProviderUserID == "" {
logger.ErrorContext(ctx, "provider user id is empty")
return oidc.Profile{}, ErrOauthProfileFetchFailed
}

return profile, nil
}

Expand Down
5 changes: 5 additions & 0 deletions go/controller/workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,11 @@ func (wf *Workflows) GetOIDCProfileFromIDToken(
return oidc.Profile{}, ErrInvalidRequest
}

if profile.ProviderUserID == "" {
logger.ErrorContext(ctx, "provider user id is empty")
return oidc.Profile{}, ErrOauthProfileFetchFailed
}

return profile, nil
}

Expand Down
4 changes: 4 additions & 0 deletions go/migrations/postgres/00018_entraid-provider.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- start a transaction
BEGIN;
DELETE FROM auth.providers WHERE id = 'entraid';
COMMIT;
8 changes: 8 additions & 0 deletions go/migrations/postgres/00018_entraid-provider.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- start a transaction
BEGIN;
INSERT INTO auth.providers (id)
VALUES ('entraid')
ON CONFLICT
DO NOTHING;
COMMIT;

18 changes: 8 additions & 10 deletions go/providers/azuread.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,29 @@ import (
type AzureAD struct {
*oauth2.Config

Tenant string
ProfileURL string
CustomParams map[string]string
ProfileURL string
}

func formatAzureADURL(tenant, path string) string {
return fmt.Sprintf("https://login.microsoftonline.com/%s%s", tenant, path)
}

func NewAzureadProvider(
clientID, clientSecret, authServerURL, tenant string,
scopes []string,
) *Provider {
baseURL := "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0"

azuread := &AzureAD{
Config: &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: authServerURL + "/signin/provider/azuread/callback",
Scopes: scopes,
Endpoint: oauth2.Endpoint{ //nolint:exhaustruct
AuthURL: baseURL + "/authorize",
TokenURL: baseURL + "/token",
AuthURL: formatAzureADURL(tenant, "/oauth2/authorize?prompt=select_account"),
TokenURL: formatAzureADURL(tenant, "/oauth2/token"),
},
},
Tenant: tenant,
ProfileURL: "https://graph.microsoft.com/oidc/userinfo",
CustomParams: map[string]string{"prompt": "select_account"},
ProfileURL: formatAzureADURL(tenant, "/openid/userinfo"),
}

return NewOauth2Provider(azuread)
Expand Down
70 changes: 70 additions & 0 deletions go/providers/entraid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package providers

import (
"context"
"fmt"

"github.com/nhost/hasura-auth/go/oidc"
"golang.org/x/oauth2"
)

type EntraID struct {
*oauth2.Config

ProfileURL string
}

func NewEntraIDProvider(
clientID, clientSecret, authServerURL, tenant string,
scopes []string,
) *Provider {
baseURL := "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0"

entraid := &EntraID{
Config: &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: authServerURL + "/signin/provider/entraid/callback",
Scopes: scopes,
Endpoint: oauth2.Endpoint{ //nolint:exhaustruct
AuthURL: baseURL + "/authorize",
TokenURL: baseURL + "/token",
},
},
ProfileURL: "https://graph.microsoft.com/oidc/userinfo",
}

return NewOauth2Provider(entraid)
}

type entraidUser struct {
Sub string `json:"sub"`
GivenName string `json:"givenname"`
FamilyName string `json:"familyname"`
Email string `json:"email"`
}

func (a *EntraID) GetProfile(
ctx context.Context,
accessToken string,
_ *string,
_ map[string]any,
) (oidc.Profile, error) {
var userProfile entraidUser
if err := fetchOAuthProfile(
ctx,
a.ProfileURL,
accessToken,
&userProfile,
); err != nil {
return oidc.Profile{}, fmt.Errorf("EntraID API error: %w", err)
}

return oidc.Profile{
ProviderUserID: userProfile.Sub,
Email: userProfile.Email,
EmailVerified: userProfile.Email != "",
Name: userProfile.GivenName + " " + userProfile.FamilyName,
Picture: "",
}, nil
}
5 changes: 4 additions & 1 deletion go/providers/scopes.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ var (
DefaultWorkOSScopes = []string{""}

// DefaultAzureadScopes defines the default scopes for AzureAd OAuth2.
DefaultAzureadScopes = []string{"openid", "email", "profile"}
DefaultAzureadScopes = []string{"email", "profile", "openid", "offline_access"}

// DefaultEntraIDScopes defines the default scopes for EntraID OAuth2.
DefaultEntraIDScopes = []string{"email", "profile", "openid", "offline_access"}

// DefaultFacebookScopes defines the default scopes for Facebook OAuth2.
DefaultFacebookScopes = []string{"email"}
Expand Down
Loading