move azdevicecode

This commit is contained in:
Boshi Lian 2022-07-04 14:06:55 +00:00
parent 8d56673982
commit 54d1f08ade
13 changed files with 99 additions and 676 deletions

View file

@ -0,0 +1,99 @@
package main
import (
"context"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
azidentity "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
a "github.com/microsoft/kiota-authentication-azure-go"
msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go"
log "github.com/sirupsen/logrus"
"github.com/tg123/sshpiper/libplugin"
"github.com/urfave/cli/v2"
)
func main() {
libplugin.CreateAndRunPluginTemplate(&libplugin.PluginTemplate{
Name: "azdevicecode",
Usage: "sshpiperd azure devicecode plugin, use devicecode to before ssh, see https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "tenant-id",
Usage: "Azure AD tenant id",
EnvVars: []string{"SSHPIPERD_AZDEVICECODE_TENANT_ID"},
Required: true,
},
&cli.StringFlag{
Name: "client-id",
Usage: "Azure AD client id",
EnvVars: []string{"SSHPIPERD_AZDEVICECODE_CLIENT_ID"},
Required: true,
},
&cli.BoolFlag{
Name: "no-read-graph",
Usage: "disable query user info from user graph",
EnvVars: []string{"SSHPIPERD_AZDEVICECODE_NOREADGRAPH"},
},
&cli.StringFlag{
Name: "scope",
Usage: "permission scope when querying user info",
EnvVars: []string{"SSHPIPERD_AZDEVICECODE_SCOPE"},
Value: "User.Read",
},
},
CreateConfig: func(c *cli.Context) (*libplugin.SshPiperPluginConfig, error) {
return &libplugin.SshPiperPluginConfig{
KeyboardInteractiveCallback: func(conn libplugin.ConnMetadata, client libplugin.KeyboardInteractiveChallenge) (*libplugin.Upstream, error) {
cred, err := azidentity.NewDeviceCodeCredential(&azidentity.DeviceCodeCredentialOptions{
TenantID: c.String("tenant-id"),
ClientID: c.String("client-id"),
UserPrompt: func(ctx context.Context, message azidentity.DeviceCodeMessage) error {
_, err := client(message.Message, "", false)
return err
},
})
if err != nil {
return nil, err
}
if c.Bool("no-read-graph") {
_, err = cred.GetToken(context.Background(), policy.TokenRequestOptions{
Scopes: []string{c.String("scope")},
})
return nil, err
}
auth, err := a.NewAzureIdentityAuthenticationProviderWithScopes(cred, []string{c.String("scope")})
if err != nil {
return nil, err
}
adapter, err := msgraphsdk.NewGraphRequestAdapter(auth)
if err != nil {
return nil, err
}
gsclient := msgraphsdk.NewGraphServiceClient(adapter)
result, err := gsclient.Me().Get()
if err != nil {
return nil, err
}
userId := *result.GetId()
log.Infof("success with challenged username: %s", userId)
return &libplugin.Upstream{
Auth: libplugin.CreateNextPluginAuth(map[string]string{
"UserId": *result.GetId(),
}),
}, nil
},
}, nil
},
})
}

View file

@ -1,21 +0,0 @@
package authy
import (
"github.com/tg123/sshpiper/sshpiperd/challenger"
)
func (authyClient) GetName() string {
return "authy"
}
func (a *authyClient) GetOpts() interface{} {
return &a.Config
}
func (a *authyClient) GetHandler() challenger.Handler {
return a.challenge
}
func init() {
challenger.Register("authy", &authyClient{})
}

View file

@ -1,31 +0,0 @@
package authy
import (
"bufio"
"fmt"
"os"
"strings"
)
func (a authyClient) findAuthyID(user string) (string, error) {
// TODO a better way to handle large database
file, err := os.Open(a.Config.File)
if err != nil {
return "", err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) >= 2 {
if fields[0] == user {
return fields[1], nil
}
}
}
return "", fmt.Errorf("authy id for user not found")
}

View file

@ -1,52 +0,0 @@
package authy
import (
"io/ioutil"
"os"
"testing"
)
func Test_findAuthyId(t *testing.T) {
a := authyClient{}
tmpfile, err := ioutil.TempFile("", "authyid")
if err != nil {
t.Fatalf("cannot create temp file %v", err)
}
defer os.Remove(tmpfile.Name())
a.Config.File = tmpfile.Name()
if err := ioutil.WriteFile(tmpfile.Name(), []byte(`
piper 123
hook 456
hook 789
`), os.ModePerm); err != nil {
t.Fatal(err)
}
{
id, err := a.findAuthyID("piper")
if err != nil {
t.Fatalf("findId failed %v", err)
}
if id != "123" {
t.Error("find id return wrong value")
}
}
{
id, err := a.findAuthyID("hook")
if err != nil {
t.Fatalf("findId failed %v", err)
}
if id != "456" {
t.Error("find id return wrong value")
}
}
}

View file

@ -1,102 +0,0 @@
package authy
import (
"fmt"
"net/url"
"time"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"github.com/dcu/go-authy"
)
type authyClient struct {
Config struct {
APIKey string `long:"challenger-authy-apikey" description:"Authy API Key" env:"SSHPIPERD_CHALLENGER_AUTHY_APIKEY" ini-name:"challenger-authy-apikey"`
// Method string `long:"challenger-authy-method" default:"token" description:"Authy authentication method" env:"SSHPIPERD_CHALLENGER_AUTHY_METHOD" ini-name:"challenger-authy-method" choice:"token" choice:"onetouch"`
Method string `long:"challenger-authy-method" default:"token" description:"Authy authentication method" env:"SSHPIPERD_CHALLENGER_AUTHY_METHOD" ini-name:"challenger-authy-method" choice:"token"`
File string `long:"challenger-authy-idfile" description:"Path to a file with ssh_name [space] authy_id per line (first line win if duplicate)" env:"SSHPIPERD_CHALLENGER_AUTHY_IDFILE" ini-name:"challenger-authy-idfile"`
}
authyAPI *authy.Authy
logger *log.Logger
}
func (a *authyClient) Init(logger *log.Logger) error {
a.logger = logger
a.authyAPI = authy.NewAuthyAPI(a.Config.APIKey)
return nil
}
func (a *authyClient) challenge(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (ssh.AdditionalChallengeContext, error) {
user := conn.User()
authyID, err := a.findAuthyID(user)
if err != nil {
return nil, err
}
switch a.Config.Method {
case "token":
ans, err := client(user, "", []string{"Please input your Authy token: "}, []bool{true})
if err != nil {
return nil, err
}
verification, err := a.authyAPI.VerifyToken(authyID, ans[0], url.Values{})
if err != nil {
return nil, err
}
if verification.Valid() {
return nil, nil
}
_, err = client(conn.User(), verification.Message, nil, nil)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("failed to auth with authy: %v", verification.Message)
case "onetouch":
_, err = client(conn.User(), "Please verify login on your Authy app", nil, nil)
if err != nil {
return nil, err
}
details := authy.Details{
"User": user,
"ClientIP": conn.RemoteAddr().String(),
}
approvalRequest, err := a.authyAPI.SendApprovalRequest(authyID, "Log to SSH server", details, url.Values{})
if err != nil {
return nil, err
}
status, err := a.authyAPI.WaitForApprovalRequest(approvalRequest.UUID, time.Second*30, url.Values{})
if err != nil {
return nil, err
}
if status == authy.OneTouchStatusApproved {
return nil, nil
}
_, err = client(conn.User(), "Authy OneTouch failed", nil, nil)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("one touch faild code: %v", status)
default:
return nil, fmt.Errorf("unsupported authy method")
}
}

View file

@ -1,21 +0,0 @@
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{})
}

View file

@ -1,88 +0,0 @@
package azdevicecode
import (
"context"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
azidentity "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
a "github.com/microsoft/kiota-authentication-azure-go"
msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go"
"github.com/microsoftgraph/msgraph-sdk-go/models"
)
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"`
Scope string `long:"challenger-azdevicecode-scope" default:"User.Read" description:"Permission scope when querying user info" env:"SSHPIPERD_CHALLENGER_AZDEVICECODE_SCOPE" ini-name:"challenger-azdevicecode-scope"`
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
}
func (c *authClient) Init(logger *log.Logger) error {
c.logger = logger
return nil
}
type aadUser struct {
models.Userable
}
func (*aadUser) ChallengerName() string {
return "azdevicecode"
}
func (a *aadUser) Meta() interface{} {
return a.Userable
}
func (a *aadUser) ChallengedUsername() string {
return *a.GetId()
}
// see https://github.com/microsoftgraph/msgraph-sdk-go
func (c *authClient) challenge(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (ssh.AdditionalChallengeContext, error) {
cred, err := azidentity.NewDeviceCodeCredential(&azidentity.DeviceCodeCredentialOptions{
TenantID: c.Config.TenantID,
ClientID: c.Config.ClientID,
UserPrompt: func(ctx context.Context, message azidentity.DeviceCodeMessage) error {
_, err := client(conn.User(), message.Message, nil, nil)
return err
},
})
if err != nil {
return nil, err
}
if c.Config.NoReadGraph {
_, err = cred.GetToken(context.Background(), policy.TokenRequestOptions{
Scopes: []string{c.Config.Scope},
})
return nil, err
}
auth, err := a.NewAzureIdentityAuthenticationProviderWithScopes(cred, []string{c.Config.Scope})
if err != nil {
return nil, err
}
adapter, err := msgraphsdk.NewGraphRequestAdapter(auth)
if err != nil {
return nil, err
}
gsclient := msgraphsdk.NewGraphServiceClient(adapter)
result, err := gsclient.Me().Get()
return &aadUser{result}, err
}

View file

@ -1,43 +0,0 @@
package challenger
import (
log "github.com/sirupsen/logrus"
)
type plugin struct {
name string
init func(logger *log.Logger) error
opts interface{}
gethandler func() Handler
}
func (p *plugin) GetName() string {
return p.name
}
func (p *plugin) GetOpts() interface{} {
return p.opts
}
func (p *plugin) GetHandler() Handler {
return p.gethandler()
}
func (p *plugin) Init(logger *log.Logger) error {
logger.Printf("challenger: %v init", p.name)
if p.init != nil {
return p.init(logger)
}
return nil
}
// NewFromHandler creates a Challenger with given functions
func NewFromHandler(name string, gethandler func() Handler, opts interface{}, init func(glogger *log.Logger) error) Provider {
return &plugin{
name: name,
init: init,
opts: opts,
gethandler: gethandler,
}
}

View file

@ -1,107 +0,0 @@
package pome
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/google/uuid"
"golang.org/x/crypto/ssh"
)
func (p *pome) load(ctx context.Context, id string) (*pipe, error) {
req, err := http.NewRequest("GET", p.Config.CheckBaseURL+id, nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode > 299 {
return nil, fmt.Errorf("bad http state code %v", resp.StatusCode)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
pipe := pipe{}
if err := json.Unmarshal(body, &pipe); err != nil {
return nil, err
}
return &pipe, nil
}
func (p *pome) loadWithRetry(ctx context.Context, id string) <-chan *pipe {
c := make(chan *pipe)
go func() {
for {
select {
case <-ctx.Done():
c <- nil
return
default:
timeout, cancel := context.WithTimeout(context.Background(), time.Millisecond*5000)
defer cancel()
pipe, err := p.load(timeout, id)
if err == nil {
c <- pipe
return
}
time.Sleep(time.Millisecond * 5000)
}
}
}()
return c
}
func (p *pome) challenge(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (ssh.AdditionalChallengeContext, error) {
uid, err := uuid.NewRandom()
if err != nil {
return nil, err
}
id := uid.String()
url := p.Config.LoginBaseURL + id
say := func(msg string) error {
_, err = client(conn.User(), msg, nil, nil)
return err
}
if err := say(fmt.Sprintf("Open %v in browser to login (timeout %v senconds)", url, p.Config.Timeout)); err != nil {
return nil, err
}
d := time.Now().Add(time.Duration(p.Config.Timeout) * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
c := p.loadWithRetry(ctx, id)
pipe := <-c
if pipe == nil {
_ = say("Login timeout")
return nil, fmt.Errorf("timeout")
}
_ = say(fmt.Sprintf("Connecting to %v@%v", pipe.Username, pipe.Address))
pipe.say = say
return pipe, nil
}

View file

@ -1,62 +0,0 @@
package pome
import (
log "github.com/sirupsen/logrus"
"github.com/tg123/sshpiper/sshpiperd/challenger"
"github.com/tg123/sshpiper/sshpiperd/upstream"
)
type plugin struct {
pome
}
func (plugin) GetName() string {
return "pome"
}
func (plugin) GetOpts() interface{} {
return nil
}
func (p *plugin) Init(logger *log.Logger) error {
p.pome.logger = logger
return nil
}
func (plugin) ListPipe() ([]upstream.Pipe, error) {
return nil, nil
}
func (plugin) CreatePipe(opt upstream.CreatePipeOption) error {
return nil
}
func (plugin) RemovePipe(name string) error {
return nil
}
type challengerPlugin struct {
*plugin
}
func (p *challengerPlugin) GetHandler() challenger.Handler {
return p.challenge
}
type upstreamPlugin struct {
*plugin
}
func (p *upstreamPlugin) GetHandler() upstream.Handler {
return p.authWithPipe
}
func (p *challengerPlugin) GetOpts() interface{} {
return &p.Config
}
func init() {
p := &plugin{}
upstream.Register("pome", &upstreamPlugin{p})
challenger.Register("pome", &challengerPlugin{p})
}

View file

@ -1,39 +0,0 @@
package pome
import (
log "github.com/sirupsen/logrus"
)
type pipe struct {
Owner string `json:"owner"`
ServerID string `json:"serverId"`
Username string `json:"username"`
Address string `json:"address"`
Auth string `json:"auth"`
PrivateKey string `json:"privateKey"`
UpPassword string `json:"upPassword"`
say func(msg string) error
}
func (pipe) ChallengerName() string {
return "pome"
}
func (p pipe) Meta() interface{} {
return p
}
func (p pipe) ChallengedUsername() string {
return p.Username
}
type pome struct {
logger *log.Logger
Config struct {
LoginBaseURL string `long:"challenger-pome-loginurl" description:"Send this url/{id} to user for login" env:"SSHPIPERD_CHALLENGER_POME_LOGINURL" ini-name:"challenger-pome-loginurl"`
CheckBaseURL string `long:"challenger-pome-checkurl" description:"Call this url/{id} to retrieve login info" env:"SSHPIPERD_CHALLENGER_POME_CHECKURL" ini-name:"challenger-pome-checkurl"`
Timeout uint `long:"challenger-pome-timeout" default:"60" description:"Timeout for waiting response from checkurl" env:"SSHPIPERD_CHALLENGER_POME_TIMEOUT" ini-name:"challenger-pome-timeout"`
}
}

View file

@ -1,67 +0,0 @@
package pome
import (
"fmt"
"net"
"golang.org/x/crypto/ssh"
"github.com/tg123/sshpiper/sshpiperd/upstream"
"github.com/tg123/sshpiper/sshpiperd/utils"
)
func (p *pome) authWithPipe(conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (net.Conn, *ssh.AuthPipe, error) {
pipe, ok := challengeContext.Meta().(pipe)
if !ok {
return nil, nil, fmt.Errorf("bad pome context")
}
host, port, err := upstream.SplitHostPortForSSH(pipe.Address)
if err != nil {
return nil, nil, pipe.say("Not add Please check your configure")
}
addr := fmt.Sprintf("%v:%v", utils.FormatIPAddress(host), port)
c, err := net.Dial("tcp", addr)
if err != nil {
return nil, nil, pipe.say(fmt.Sprintf("Cannot connect to %v, reason: %v", addr, err))
}
callback := func() (ssh.AuthPipeType, ssh.AuthMethod, error) {
switch pipe.Auth {
case "key":
private, err := ssh.ParsePrivateKey([]byte(pipe.PrivateKey))
if err != nil {
return ssh.AuthPipeTypeNone, nil, err
}
return ssh.AuthPipeTypeMap, ssh.PublicKeys(private), nil
case "pass":
return ssh.AuthPipeTypeMap, ssh.Password(pipe.UpPassword), nil
}
return ssh.AuthPipeTypeNone, nil, fmt.Errorf("unsupport auth type %v", pipe.Auth)
}
return c, &ssh.AuthPipe{
User: pipe.Username,
NoneAuthCallback: func(conn ssh.ConnMetadata) (ssh.AuthPipeType, ssh.AuthMethod, error) {
return callback()
},
PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (ssh.AuthPipeType, ssh.AuthMethod, error) {
return callback()
},
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (ssh.AuthPipeType, ssh.AuthMethod, error) {
return callback()
},
UpstreamHostKeyCallback: ssh.InsecureIgnoreHostKey(),
}, nil
}

View file

@ -1,43 +0,0 @@
package challenger
import (
"golang.org/x/crypto/ssh"
"github.com/tg123/sshpiper/sshpiperd/registry"
)
// 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) (ssh.ChallengeContext, error)
// Provider is a factory for Challenger
type Provider interface {
registry.Plugin
GetHandler() Handler
}
var (
drivers = registry.NewRegistry()
)
// Register adds an challenger with given name to registry
func Register(name string, driver Provider) {
drivers.Register(name, driver)
}
// All return all registered challenger
func All() []string {
return drivers.Drivers()
}
// Get returns an challenger by name, return nil if not found
func Get(name string) Provider {
if d, ok := drivers.Get(name).(Provider); ok {
return d
}
return nil
}