Skip to content

Conversation

Nanowires
Copy link

Another PR for #601
Mostly copied from #602

Same as the original author: I'm not particularly familiar with go, or the ntfy code base, I haven't tried to run this code.

If I have time I will try to add tests in the next few days.

I'm also not sure about the way to trace here (e.g. in case that the user-header is set and the client sent one, but the user is not found)

@binwiederhier
Copy link
Owner

This looks great, thank you. Given that it is an auth change, I will test this thoroughly myself, but I would highly appreciate some unit tests around this. Check out server_test.go and look for TestServer_Auth_* tests. You can probably model them after that.

@Nanowires
Copy link
Author

Given that it is an auth change, I will test this thoroughly myself

I would expect nothing less ;-)

I would highly appreciate some unit tests around this. Check out server_test.go and look for TestServer_Auth_* tests. You can probably model them after that.

I will definitely give this a try, but as I said, this will at least take some days.

Maybe one question about performance:
Does it make sense, to do this new authentication first? As this is (in my opinion) only a rare corner case (< 1% I would think), but we would still check this path first every time.
On the other hand, if the config value isn't set, this is only one additional check against a value cached in memory...

# Otherwise the authentication can be bypassed by a crafted header.
#
# user-header: x-forwarded-user

Copy link
Owner

Choose a reason for hiding this comment

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

I think this header should only be allowed if behind-proxy: true is set.

server/server.go Outdated

// extractUserHader pulls the username of an already authenticated user from the configured header
func extractUserHeader(r *http.Request, h string) (username string) {
return readParam(r, h)
Copy link
Owner

Choose a reason for hiding this comment

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

Took me a while, but this is a giant security flaw, becausereadParam also accepts query params and not just headers. So this must be just r.Header.Get()

@binwiederhier
Copy link
Owner

I refined your code in #816. Note that your commit are not in there, because the ntfy binary (30MB) would be in the history. If you'd like your commit to be in there, feel free to force-push an updated branch, or open a new PR. You and the original author will get credit in the changelog either way though.

Please review #816 to see if this would work for you. I think it's a nice addition.

Question: What auth system are you using?

@binwiederhier
Copy link
Owner

Does it make sense, to do this new authentication first? As this is (in my opinion) only a rare corner case (< 1% I would think), but we would still check this path first every time. On the other hand, if the config value isn't set, this is only one additional check against a value cached in memory...

My code will only check the value if the config value is set. It's a single if statement. It'll be fine.

@Nanowires
Copy link
Author

Question: What auth system are you using?

I would go for a client certificate, as the number of clients is very small (just me ;-P)

@binwiederhier
Copy link
Owner

Fair warning: The ntfy Android client does not work with custom certs though.

@binwiederhier
Copy link
Owner

@Nanowires I am a little confused now. I thought that this would enable the use of Authelia or Keycloak. Am I wrong?

@Nanowires
Copy link
Author

As far as I understand, this allows any kind of authentication, as long as the proxy sends the user defined header with the associated user.

@wunter8
Copy link
Contributor

wunter8 commented Nov 3, 2023

I tested this with Authelia and Caddy tonight. Here's the config:

Caddy

ntfy-test.domain.com {
    forward_auth localhost:8010 {
        uri /api/verify?rd=https://auth.domain.com
        copy_headers Remote-User
    }

    reverse_proxy 127.0.0.1:8093
}

ntfy

base-url: https://ntfy-test.domain.com
listen-http: :8093
behind-proxy: true
auth-user-header: Remote-User
auth-file: ./user.db
auth-default-access: deny

Here are some things I found:

  1. if a person is logged into Authelia and doesn't have a corresponding ntfy account with the same username, the ntfy web app will fail immediately. it will return a 401 unauthorized for every request (including static assets). is this expected?
  2. subscribing and publishing using the web app to restricted topics worked after logging in with Authelia
  3. i tried it with enable-login: true, and I wasn't automatically signed in, meaning the "Sign In" button was still visible in the top right corner, clicking on it took me to the login form page, none of my account topics automatically loaded
  4. filling out the login form with my ntfy credentials and logging in that way did load/sync my topics
  5. websocket connections seemed to work fine in the background of the webapp, even going through Authelia
  6. the user I'm logged in as doesn't show up under Settings > Manage Users

There might be more issues, but for at least these reasons, I don't think this is ready to merge right away.

I did not test anything from the Android app. I'm not even sure how I could sign into Authelia from the ntfy app...

@NekoLuka NekoLuka mentioned this pull request Aug 19, 2024
@simonfelding
Copy link

simonfelding commented Sep 15, 2025

This would be very useful - wish this could be implemented!

@Nanowires I am a little confused now. I thought that this would enable the use of Authelia or Keycloak. Am I wrong?

Yes it would enable the use of any custom auth gateway, Authelia and Keycloak included. Enabling the auth-user-header would let a auth proxy handle auth, and then forward the request with the header set. It could be used with for example https://dexidp.io/docs/connectors/authproxy/ (would let users use LDAP, OIDC, or whatever they like)
or
https://oauth2-proxy.github.io/oauth2-proxy/ (would use OIDC)
or
https://www.authelia.com/integration/trusted-header-sso/introduction/ (authelia's documentation for the same purpose as oauth2-proxy)


For this PR to be implemented, @wunter8 has some good observations.

  1. if a person is logged into Authelia and doesn't have a corresponding ntfy account with the same username, the ntfy web app will fail immediately. it will return a 401 unauthorized for every request (including static assets). is this expected?

No, this is not expected. ntfy should accept the username, even if there is no ntfy account with that name. A benefit of this is that the auth-proxy that sets the auth-user-header could use either Remote-User or Remote-Group - which would be a really cool and simple way to have a single ACL entry for a whole group of users, without having to change the ACL code (because the group-name would just be used as the user-name by ntfy).

  1. subscribing and publishing using the web app to restricted topics worked after logging in with Authelia

As expected - it uses webpush and vapid to subscribe to topics.

  1. i tried it with enable-login: true, and I wasn't automatically signed in, meaning the "Sign In" button was still visible in the top right corner, clicking on it took me to the login form page, none of my account topics automatically loaded

This seems related to 1 - auth-header authentication should have first priority if enabled.

  1. filling out the login form with my ntfy credentials and logging in that way did load/sync my topics

Unrelated. Login should work without the auth-header set, even if enabled. This allows for multiple login options, so token auth and user auth can still work even if the auth header option is enabled.

  1. websocket connections seemed to work fine in the background of the webapp, even going through Authelia

As expected.

  1. the user I'm logged in as doesn't show up under Settings > Manage Users

It really shouldn't either, it's not managed by ntfy in this use case.

I did not test anything from the Android app. I'm not even sure how I could sign into Authelia from the ntfy app...

When the app starts, it should check if it is authenticated. If it receives a HTTP 3XX Redirect response, it should open a webview to follow the redirect. This would let the auth proxy do it's auth thing and then close the webview window with javascript using window.close() - ntfy doesn't need to handle this, it just needs to wait for the webview to end, and then retry the request.


@binwiederhier any chance these comments could help get this implemented? It would be extremely useful for large-scale deployments that need authentication with a centralized user management system like LDAP or OIDC.

Copy link

@simonfelding simonfelding left a comment

Choose a reason for hiding this comment

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

This needs to be changed so it supports authentication even if the user does not exist (that would enable the oidc auth proxy use-case).

Comment on lines +1882 to +1902
// authenticateViaUserDefinedHeader tries to authenticate the user via the header defined in the "auth-user-header"
// configuration value if it is set. The value of the passed username is used to lookup the user in the database.
// If it exists, authentication is successful.
//
// This function will ALWAYS return a visitor, even if an error occurs (e.g. unauthorized), so
// that subsequent logging calls still have a visitor context.
func (s *Server) authenticateViaUserDefinedHeader(r *http.Request, vip *visitor, username string) (*visitor, error) {
// Check the rate limiter first
if !vip.AuthAllowed() {
return vip, errHTTPTooManyRequestsLimitAuthFailure // Always return visitor, even when error occurs!
}
// Retrieve user from database; if found, we have a successful authentication
u, err := s.userManager.User(username)
if err != nil || u.Deleted {
vip.AuthFailed()
logr(r).Err(err).Debug("Authentication failed")
return vip, errHTTPUnauthorized
}
// User was found, meaning that auth was successful
return s.visitor(vip.ip, u), nil
}

Choose a reason for hiding this comment

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

This code should also create the user if it doesn't exist. It could assign a random password if needed. That's all there needs to be done to make it work properly as far as I can tell.

Choose a reason for hiding this comment

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

or at least have an option to allow automatic user registration through the http auth header.

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.

4 participants