From 374f474c10b97731ab0fc5cc97bea31d90057dc9 Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Wed, 9 Jan 2019 03:16:02 -0800 Subject: [PATCH] introduce az device code challenger --- .../challenger/azdevicecode/azdevicecode.go | 21 ++++ sshpiperd/challenger/azdevicecode/handler.go | 100 ++++++++++++++++++ sshpiperd/challenger/pam/pam_challenger.go | 8 +- sshpiperd/challenger/provider.go | 2 +- .../challenger/welcometext/welcometext.go | 4 +- sshpiperd/loader.go | 1 + sshpiperd/upstream/database/handler.go | 2 +- sshpiperd/upstream/provider.go | 2 +- sshpiperd/upstream/workingdir/workingdir.go | 2 +- 9 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 sshpiperd/challenger/azdevicecode/azdevicecode.go create mode 100644 sshpiperd/challenger/azdevicecode/handler.go diff --git a/sshpiperd/challenger/azdevicecode/azdevicecode.go b/sshpiperd/challenger/azdevicecode/azdevicecode.go new file mode 100644 index 00000000..3b86f3b3 --- /dev/null +++ b/sshpiperd/challenger/azdevicecode/azdevicecode.go @@ -0,0 +1,21 @@ +package azdevicecode + +import ( + "github.com/tg123/sshpiper/sshpiperd/challenger" +) + +func (*authClient) GetName() string { + return "azdevicecode" +} + +func (c *authClient) GetOpts() interface{} { + return &c.Config +} + +func (c *authClient) GetHandler() challenger.Handler { + return c.challenge +} + +func init() { + challenger.Register("azdevicecode", &authClient{}) +} diff --git a/sshpiperd/challenger/azdevicecode/handler.go b/sshpiperd/challenger/azdevicecode/handler.go new file mode 100644 index 00000000..f975fda5 --- /dev/null +++ b/sshpiperd/challenger/azdevicecode/handler.go @@ -0,0 +1,100 @@ +package azdevicecode + +import ( + "context" + "log" + "net/http" + + "golang.org/x/crypto/ssh" + + "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/adal" + "github.com/Azure/go-autorest/autorest/azure" +) + +type authClient struct { + Config struct { + TenantId string `long:"challenger-azdevicecode-tenantid" description:"Azure AD tenant id" env:"SSHPIPERD_CHALLENGER_AZDEVICECODE_TENANTID" ini-name:"challenger-azdevicecode-tenantid"` + ClientId string `long:"challenger-azdevicecode-clientid" description:"Azure AD client id" env:"SSHPIPERD_CHALLENGER_AZDEVICECODE_CLIENTID" ini-name:"challenger-azdevicecode-clientid"` + Env string `long:"challenger-azdevicecode-env" default:"AzurePublicCloud" description:"Azure AD Cloud to request" env:"SSHPIPERD_CHALLENGER_AZDEVICECODE_ENV" ini-name:"challenger-azdevicecode-env" choice:"AzureChinaCloud" choice:"AzureGermanCloud" choice:"AzurePublicCloud" choice:"AzureUSGovernmentCloud"` + Resource string `long:"challenger-azdevicecode-resource" default:"https://graph.windows.net/" description:"Resource URI to access, default is Graph API" env:"SSHPIPERD_CHALLENGER_AZDEVICECODE_RESOURCE" ini-name:"challenger-azdevicecode-resource"` + NoReadGraph bool `long:"challenger-azdevicecode-noreadgraph" description:"disable query user info from user graph" env:"SSHPIPERD_CHALLENGER_AZDEVICECODE_NOREADGRAPH" ini-name:"challenger-azdevicecode-noreadgraph"` + } + + logger *log.Logger + oauthConfig adal.OAuthConfig +} + +func (c *authClient) Init(logger *log.Logger) error { + c.logger = logger + + env, err := azure.EnvironmentFromName(c.Config.Env) + if err != nil { + return err + } + + oauthConfig, err := adal.NewOAuthConfig(env.ActiveDirectoryEndpoint, c.Config.TenantId) + if err != nil { + return err + } + + c.oauthConfig = *oauthConfig + + return nil +} + +type aadUser struct { + user graphrbac.User +} + +func (*aadUser) ChallengerName() string { + return "azdevicecode" +} + +func (a *aadUser) Meta() interface{} { + return a.user +} + +func (a *aadUser) ChallengedUsername() string { + return *a.user.UserPrincipalName +} + +func (c *authClient) challenge(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (ssh.AdditionalChallengeContext, error) { + oauthClient := &http.Client{} + deviceCode, err := adal.InitiateDeviceAuth(oauthClient, c.oauthConfig, c.Config.ClientId, c.Config.Resource) + + if err != nil { + return nil, err + } + + _, err = client(conn.User(), *deviceCode.Message, nil, nil) + if err != nil { + return nil, err + } + + token, err := adal.WaitForUserCompletion(oauthClient, deviceCode) + if err != nil { + return nil, err + } + + // skip read user info from graph api + if c.Config.NoReadGraph { + return nil, nil + } + + spt, err := adal.NewServicePrincipalTokenFromManualToken(c.oauthConfig, c.Config.ClientId, c.Config.Resource, *token) + if err != nil { + return nil, err + } + + signedInUserClient := graphrbac.NewSignedInUserClient(c.Config.TenantId) + signedInUserClient.Authorizer = autorest.NewBearerAuthorizer(spt) + + result, err := signedInUserClient.Get(context.TODO()) + if err != nil { + return nil, err + } + + return &aadUser{user: result}, nil +} diff --git a/sshpiperd/challenger/pam/pam_challenger.go b/sshpiperd/challenger/pam/pam_challenger.go index 0ab73fbf..ec692475 100644 --- a/sshpiperd/challenger/pam/pam_challenger.go +++ b/sshpiperd/challenger/pam/pam_challenger.go @@ -18,7 +18,7 @@ const ( SSHPIPER_PAM_SERVICE_FILE = "/etc/pam.d/sshpiperd" ) -func pamChallenger(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (bool, error) { +func pamChallenger(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (ssh.AdditionalChallengeContext, error) { user := conn.User() @@ -52,15 +52,15 @@ func pamChallenger(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChalleng }) if err != nil { - return false, err + return nil, err } err = t.Authenticate(0) if err != nil { - return false, err + return nil, err } - return true, nil + return nil, nil } func init() { diff --git a/sshpiperd/challenger/provider.go b/sshpiperd/challenger/provider.go index 60a0323a..9a0ec8d8 100644 --- a/sshpiperd/challenger/provider.go +++ b/sshpiperd/challenger/provider.go @@ -9,7 +9,7 @@ import ( // Handler is the callback for additional challenger // use args client ssh.KeyboardInteractiveChallenge to interact with downstream // return bool to indicate whether if the challenge is passed -type Handler func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (bool, error) +type Handler func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (ssh.AdditionalChallengeContext, error) // Provider is a factory for Challenger type Provider interface { diff --git a/sshpiperd/challenger/welcometext/welcometext.go b/sshpiperd/challenger/welcometext/welcometext.go index 67914984..8ba0def5 100644 --- a/sshpiperd/challenger/welcometext/welcometext.go +++ b/sshpiperd/challenger/welcometext/welcometext.go @@ -9,11 +9,11 @@ import ( ) func makeWelcomeChallenger(text string) challenger.Handler { - return func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (bool, error) { + return func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (ssh.AdditionalChallengeContext, error) { client(conn.User(), text, nil, nil) - return true, nil + return nil, nil } } diff --git a/sshpiperd/loader.go b/sshpiperd/loader.go index 9f07e260..362bbb60 100644 --- a/sshpiperd/loader.go +++ b/sshpiperd/loader.go @@ -4,6 +4,7 @@ import ( _ "github.com/tg123/sshpiper/sshpiperd/upstream/database" _ "github.com/tg123/sshpiper/sshpiperd/upstream/workingdir" + _ "github.com/tg123/sshpiper/sshpiperd/challenger/azdevicecode" _ "github.com/tg123/sshpiper/sshpiperd/challenger/pam" _ "github.com/tg123/sshpiper/sshpiperd/challenger/welcometext" diff --git a/sshpiperd/upstream/database/handler.go b/sshpiperd/upstream/database/handler.go index 427668f8..d2316745 100644 --- a/sshpiperd/upstream/database/handler.go +++ b/sshpiperd/upstream/database/handler.go @@ -8,7 +8,7 @@ import ( "golang.org/x/crypto/ssh" ) -func (p *plugin) findUpstream(conn ssh.ConnMetadata) (net.Conn, *ssh.AuthPipe, error) { +func (p *plugin) findUpstream(conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (net.Conn, *ssh.AuthPipe, error) { user := conn.User() d, err := lookupDownstreamWithFallback(p.db, user) diff --git a/sshpiperd/upstream/provider.go b/sshpiperd/upstream/provider.go index 947d41ac..148eb385 100644 --- a/sshpiperd/upstream/provider.go +++ b/sshpiperd/upstream/provider.go @@ -14,7 +14,7 @@ import ( // the returned auth pipe is to map/convert downstream auth method to another auth for // connecting to upstream. // e.g. map downstream public key to another upstream private key -type Handler func(conn ssh.ConnMetadata) (net.Conn, *ssh.AuthPipe, error) +type Handler func(conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (net.Conn, *ssh.AuthPipe, error) type CreatePipeOption struct { Username string diff --git a/sshpiperd/upstream/workingdir/workingdir.go b/sshpiperd/upstream/workingdir/workingdir.go index d2db5bca..52850ad3 100644 --- a/sshpiperd/upstream/workingdir/workingdir.go +++ b/sshpiperd/upstream/workingdir/workingdir.go @@ -112,7 +112,7 @@ func parseUpstreamFile(data string) (host string, port int, user string, err err return } -func findUpstreamFromUserfile(conn ssh.ConnMetadata) (net.Conn, *ssh.AuthPipe, error) { +func findUpstreamFromUserfile(conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (net.Conn, *ssh.AuthPipe, error) { user := conn.User() if !checkUsername(user) {