Problem
When an HTTP login plugin is configured, frps calls the plugin first and then always runs the built-in VerifyLogin check afterward (server/service.go). There is no way to configure frps to skip the built-in check and delegate authentication entirely to the plugin.
This makes it impossible to implement the natural use case: a login plugin that validates a bearer token (e.g. a signed JWT) without also maintaining a pre-shared token on frps.
Concrete example
- frpc uses
auth.method = oidc + auth.oidc.tokenSource = exec to fetch a signed JWT dynamically and send it as privilege_key
- A login plugin validates the JWT (checks signature, issuer, audience, claims)
- frps rejects the connection anyway because
VerifyLogin computes MD5(server_token + timestamp) and it does not match the JWT string
As a workaround, I currently make my plugin rewrite privilege_key in its response to MD5("" + str(timestamp))
Proposed solution
Add auth.method = none to AuthServerConfig. When set, NewAuthVerifier returns AlwaysPassVerifier, making VerifyLogin a no-op. Authentication is then fully delegated to any registered login plugins.
The change is small and non-breaking:
pkg/config/v1/common.go: add AuthMethodNone AuthMethod = none
pkg/config/v1/validation/validation.go: add none to SupportedAuthMethods
pkg/auth/auth.go: add case v1.AuthMethodNone: authVerifier = AlwaysPassVerifier
pkg/config/v1/server.go: keep default as token so existing configs are unaffected
frps config usage:
[auth]
method = "none"
[[httpPlugins]]
name = "my-auth-plugin"
addr = "http://127.0.0.1:8080"
ops = ["Login"]
Why this is safe
A login plugin already has full control over whether a connection is accepted or rejected. VerifyLogin running afterward adds nothing when a login plugin is registered: the plugin is the gate. auth.method = none simply makes that explicit and removes the redundant check.
Affected area
Problem
When an HTTP login plugin is configured, frps calls the plugin first and then always runs the built-in
VerifyLogincheck afterward (server/service.go). There is no way to configure frps to skip the built-in check and delegate authentication entirely to the plugin.This makes it impossible to implement the natural use case: a login plugin that validates a bearer token (e.g. a signed JWT) without also maintaining a pre-shared token on frps.
Concrete example
auth.method = oidc+auth.oidc.tokenSource = execto fetch a signed JWT dynamically and send it asprivilege_keyVerifyLogincomputesMD5(server_token + timestamp)and it does not match the JWT stringAs a workaround, I currently make my plugin rewrite
privilege_keyin its response toMD5("" + str(timestamp))Proposed solution
Add
auth.method = nonetoAuthServerConfig. When set,NewAuthVerifierreturnsAlwaysPassVerifier, makingVerifyLogina no-op. Authentication is then fully delegated to any registered login plugins.The change is small and non-breaking:
pkg/config/v1/common.go: addAuthMethodNone AuthMethod = nonepkg/config/v1/validation/validation.go: addnonetoSupportedAuthMethodspkg/auth/auth.go: addcase v1.AuthMethodNone:authVerifier = AlwaysPassVerifierpkg/config/v1/server.go: keep default astokenso existing configs are unaffectedfrps config usage:
Why this is safe
A login plugin already has full control over whether a connection is accepted or rejected.
VerifyLoginrunning afterward adds nothing when a login plugin is registered: the plugin is the gate.auth.method = nonesimply makes that explicit and removes the redundant check.Affected area