mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-07-04 17:06:49 +00:00
Oauth2 support
This commit is contained in:
@ -70,7 +70,7 @@ func ldapSearchUser(l *ldap3.Conn, ldapSettings setting.LDAPSettings, username s
|
||||
0,
|
||||
false,
|
||||
strings.Replace(ldapSettings.SelfFilter, "{{USERNAME}}", username, 1),
|
||||
nil, // []string{"name"},
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
const (
|
||||
TypeLocal = "local"
|
||||
TypeLDAP = "ldap"
|
||||
TypeOIDC = "oidc"
|
||||
TypeOAuth = "oauth"
|
||||
)
|
||||
|
||||
// Model is the model
|
||||
|
216
backend/internal/entity/auth/oauth.go
Normal file
216
backend/internal/entity/auth/oauth.go
Normal file
@ -0,0 +1,216 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"npm/internal/entity/setting"
|
||||
"npm/internal/logger"
|
||||
|
||||
cache "github.com/patrickmn/go-cache"
|
||||
"github.com/rotisserie/eris"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// AuthCache is a cache item that stores the Admin API data for each admin that has been requesting endpoints
|
||||
var OAuthCache *cache.Cache
|
||||
|
||||
// OAuthCacheInit will create a new Memory Cache
|
||||
func OAuthCacheInit() {
|
||||
if OAuthCache == nil {
|
||||
logger.Debug("Creating a new OAuthCache")
|
||||
OAuthCache = cache.New(5*time.Minute, 5*time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
// OAuthUser is the OAuth User
|
||||
type OAuthUser struct {
|
||||
Identifier string `json:"identifier"`
|
||||
Token string `json:"token"`
|
||||
Resource map[string]interface{} `json:"resource"`
|
||||
}
|
||||
|
||||
// GetEmail will return an email address even if it can't be known in the
|
||||
// Resource
|
||||
func (m *OAuthUser) GetResourceField(field string) string {
|
||||
if m.Resource != nil {
|
||||
if value, ok := m.Resource[field]; ok {
|
||||
return value.(string)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetEmail will return an email address even if it can't be known in the
|
||||
// Resource
|
||||
func (m *OAuthUser) GetID() string {
|
||||
if m.Identifier != "" {
|
||||
return m.Identifier
|
||||
}
|
||||
|
||||
fields := []string{
|
||||
"uid",
|
||||
"user_id",
|
||||
"username",
|
||||
"preferred_username",
|
||||
"email",
|
||||
"mail",
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
if val := m.GetResourceField(field); val != "" {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetName attempts to get a name from the resource
|
||||
// using different fields
|
||||
func (m *OAuthUser) GetName() string {
|
||||
fields := []string{
|
||||
"nickname",
|
||||
"given_name",
|
||||
"name",
|
||||
"preferred_username",
|
||||
"username",
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
if name := m.GetResourceField(field); name != "" {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback:
|
||||
return m.Identifier
|
||||
}
|
||||
|
||||
// GetEmail will return an email address even if it can't be known in the
|
||||
// Resource
|
||||
func (m *OAuthUser) GetEmail() string {
|
||||
// See if there's an email field first
|
||||
if email := m.GetResourceField("email"); email != "" {
|
||||
return email
|
||||
}
|
||||
|
||||
// Return the identifier if it looks like an email
|
||||
if m.Identifier != "" {
|
||||
if strings.Contains(m.Identifier, "@") {
|
||||
return m.Identifier
|
||||
}
|
||||
return fmt.Sprintf("%s@oauth", m.Identifier)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getOAuth2Config() (*oauth2.Config, *setting.OAuthSettings, error) {
|
||||
oauthSettings, err := setting.GetOAuthSettings()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if oauthSettings.ClientID == "" || oauthSettings.ClientSecret == "" || oauthSettings.AuthURL == "" || oauthSettings.TokenURL == "" {
|
||||
return nil, nil, eris.New("oauth-settings-incorrect")
|
||||
}
|
||||
|
||||
return &oauth2.Config{
|
||||
ClientID: oauthSettings.ClientID,
|
||||
ClientSecret: oauthSettings.ClientSecret,
|
||||
Scopes: oauthSettings.Scopes,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: oauthSettings.AuthURL,
|
||||
TokenURL: oauthSettings.TokenURL,
|
||||
},
|
||||
}, &oauthSettings, nil
|
||||
}
|
||||
|
||||
// OAuthLogin ...
|
||||
func OAuthLogin(redirectBase, ipAddress string) (string, error) {
|
||||
OAuthCacheInit()
|
||||
|
||||
conf, _, err := getOAuth2Config()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
verifier := oauth2.GenerateVerifier()
|
||||
OAuthCache.Set(getCacheKey(ipAddress), verifier, cache.DefaultExpiration)
|
||||
|
||||
// todo: state should be unique to the incoming IP address of the requester, I guess
|
||||
url := conf.AuthCodeURL("state", oauth2.AccessTypeOnline, oauth2.S256ChallengeOption(verifier))
|
||||
|
||||
if redirectBase != "" {
|
||||
url = url + "&redirect_uri=" + redirectBase + "/oauth/redirect"
|
||||
}
|
||||
|
||||
logger.Debug("URL: %s", url)
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// OAuthReturn ...
|
||||
func OAuthReturn(ctx context.Context, code, ipAddress string) (*OAuthUser, error) {
|
||||
// Just in case...
|
||||
OAuthCacheInit()
|
||||
|
||||
conf, oauthSettings, err := getOAuth2Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
verifier, found := OAuthCache.Get(getCacheKey(ipAddress))
|
||||
if !found {
|
||||
return nil, eris.New("oauth-verifier-not-found")
|
||||
}
|
||||
|
||||
// Use the authorization code that is pushed to the redirect
|
||||
// URL. Exchange will do the handshake to retrieve the
|
||||
// initial access token. The HTTP Client returned by
|
||||
// conf.Client will refresh the token as necessary.
|
||||
tok, err := conf.Exchange(ctx, code, oauth2.VerifierOption(verifier.(string)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// At this stage, the token is the JWT as given by the oauth server.
|
||||
// we need to use that to get more info about this user,
|
||||
// and then we'll create our own jwt for use later.
|
||||
|
||||
client := conf.Client(ctx, tok)
|
||||
resp, err := client.Get(oauthSettings.ResourceURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// nolint: errcheck, gosec
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ou := OAuthUser{
|
||||
Token: tok.AccessToken,
|
||||
}
|
||||
|
||||
// unmarshal the body into a interface
|
||||
if err := json.Unmarshal(body, &ou.Resource); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Attempt to get the identifier from the resource
|
||||
if oauthSettings.Identifier != "" {
|
||||
ou.Identifier = ou.GetResourceField(oauthSettings.Identifier)
|
||||
}
|
||||
|
||||
return &ou, nil
|
||||
}
|
||||
|
||||
func getCacheKey(ipAddress string) string {
|
||||
return fmt.Sprintf("oauth-%s", ipAddress)
|
||||
}
|
@ -2,19 +2,31 @@ package setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// GetAuthMethods returns the authentication methods enabled for this site
|
||||
func GetAuthMethods() ([]string, error) {
|
||||
var l []string
|
||||
var m Model
|
||||
if err := m.LoadByName("auth-methods"); err != nil {
|
||||
return l, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(m.Value.String()), &l); err != nil {
|
||||
return l, err
|
||||
var r []string
|
||||
if err := json.Unmarshal([]byte(m.Value.String()), &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l, nil
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// AuthMethodEnabled checks that the auth method given is
|
||||
// enabled in the db setting
|
||||
func AuthMethodEnabled(method string) bool {
|
||||
r, err := GetAuthMethods()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return slices.Contains(r, method)
|
||||
}
|
||||
|
42
backend/internal/entity/setting/oauth.go
Normal file
42
backend/internal/entity/setting/oauth.go
Normal file
@ -0,0 +1,42 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// OAuthSettings are the settings for OAuth that come from
|
||||
// the `oauth-auth` setting value
|
||||
type OAuthSettings struct {
|
||||
AutoCreateUser bool `json:"auto_create_user"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
AuthURL string `json:"authorization_url"`
|
||||
TokenURL string `json:"token_url"`
|
||||
Identifier string `json:"identifier"`
|
||||
LogoutURL string `json:"logout_url"`
|
||||
Scopes []string `json:"scopes"`
|
||||
ResourceURL string `json:"resource_url"`
|
||||
}
|
||||
|
||||
// GetOAuthSettings will return the OAuth settings
|
||||
func GetOAuthSettings() (OAuthSettings, error) {
|
||||
var o OAuthSettings
|
||||
var m Model
|
||||
if err := m.LoadByName("oauth-auth"); err != nil {
|
||||
return o, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(m.Value.String()), &o); err != nil {
|
||||
return o, err
|
||||
}
|
||||
|
||||
o.ApplyDefaults()
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// ApplyDefaults will ensure there are defaults set
|
||||
func (m *OAuthSettings) ApplyDefaults() {
|
||||
if m.Identifier == "" {
|
||||
m.Identifier = "email"
|
||||
}
|
||||
}
|
@ -117,3 +117,14 @@ func CreateFromLDAPUser(ldapUser *auth.LDAPUser) (Model, error) {
|
||||
user.generateGravatar()
|
||||
return user, err
|
||||
}
|
||||
|
||||
// CreateFromOAuthUser will create a user from an OAuth user object
|
||||
func CreateFromOAuthUser(ou *auth.OAuthUser) (Model, error) {
|
||||
user := Model{
|
||||
Email: ou.GetEmail(),
|
||||
Name: ou.GetName(),
|
||||
}
|
||||
err := user.Save()
|
||||
user.generateGravatar()
|
||||
return user, err
|
||||
}
|
||||
|
Reference in New Issue
Block a user