~nskaggs/+junk/xenial-test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
// Copyright 2016 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.

// Package ussologin defines functionality used for allowing clients
// to authenticate with the IDM server using USSO OAuth.
package ussologin

import (
	"encoding/json"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"

	"github.com/juju/httprequest"
	"github.com/juju/usso"
	"gopkg.in/errgo.v1"
	"gopkg.in/juju/environschema.v1"
	"gopkg.in/juju/environschema.v1/form"
	"gopkg.in/macaroon-bakery.v1/httpbakery"
)

type tokenGetter interface {
	GetTokenWithOTP(username, password, otp, tokenName string) (*usso.SSOData, error)
}

// This is defined here to allow it to be stubbed out in tests
var server tokenGetter = usso.ProductionUbuntuSSOServer

var (
	userKey = "E-Mail"
	passKey = "Password"
	otpKey  = "Two-factor auth (Enter for none)"
)

// GetToken uses filler to interact with the user and uses the provided
// information to obtain an OAuth token from Ubuntu SSO. The returned
// token can subsequently be used with LoginWithToken to perform a login.
// The tokenName argument is used as the name of the generated token in
// Ubuntu SSO. If Ubuntu SSO returned an error when trying to retrieve
// the token the error will have a cause of type *usso.Error.
func GetToken(filler form.Filler, tokenName string) (*usso.SSOData, error) {
	login, err := filler.Fill(loginForm)
	if err != nil {
		return nil, errgo.Notef(err, "cannot read login parameters")
	}
	tok, err := server.GetTokenWithOTP(
		login[userKey].(string),
		login[passKey].(string),
		login[otpKey].(string),
		tokenName,
	)

	if err != nil {
		return nil, errgo.NoteMask(err, "cannot get token", isUSSOError)
	}
	return tok, nil
}

// loginForm contains the fields required for login.
var loginForm = form.Form{
	Title: "Login to Ubuntu SSO",
	Fields: environschema.Fields{
		userKey: environschema.Attr{
			Description: "Username",
			Type:        environschema.Tstring,
			Mandatory:   true,
			Group:       "1",
		},
		passKey: environschema.Attr{
			Description: "Password",
			Type:        environschema.Tstring,
			Mandatory:   true,
			Secret:      true,
			Group:       "1",
		},
		otpKey: environschema.Attr{
			Description: "Two-factor auth",
			Type:        environschema.Tstring,
			Mandatory:   true,
			Group:       "2",
		},
	},
}

// LoginWithToken completes a login attempt using tok. The ussoAuthURL
// should have been obtained from the UbuntuSSOOAuth field in a response
// to a LoginMethods request from the target service.
func LoginWithToken(client *http.Client, ussoAuthUrl string, tok *usso.SSOData) error {
	req, err := http.NewRequest("GET", ussoAuthUrl, nil)
	if err != nil {
		return errgo.Notef(err, "cannot create request")
	}
	base := *req.URL
	base.RawQuery = ""
	rp := usso.RequestParameters{
		HTTPMethod:      req.Method,
		BaseURL:         base.String(),
		Params:          req.URL.Query(),
		SignatureMethod: usso.HMACSHA1{},
	}
	if err := tok.SignRequest(&rp, req); err != nil {
		return errgo.Notef(err, "cannot sign request")
	}
	resp, err := client.Do(req)
	if err != nil {
		return errgo.Notef(err, "cannot do request")
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusOK {
		return nil
	}
	var herr httpbakery.Error
	if err := httprequest.UnmarshalJSONResponse(resp, &herr); err != nil {
		return errgo.Notef(err, "cannot unmarshal error")
	}
	return &herr
}

// TokenStore defines the interface for something that can store and returns oauth tokens.
type TokenStore interface {
	// Put stores an Ubuntu SSO OAuth token.
	Put(tok *usso.SSOData) error
	// Get returns an Ubuntu SSO OAuth token from store
	Get() (*usso.SSOData, error)
}

// FileTokenStore implements the TokenStore interface by storing the
// JSON-encoded oauth token in a file.
type FileTokenStore struct {
	path string
}

// NewFileTokenStore returns a new FileTokenStore
// that uses the given path for storage.
func NewFileTokenStore(path string) *FileTokenStore {
	return &FileTokenStore{path}
}

// Put implements TokenStore.Put by writing the token to the
// FileTokenStore's file. If the file doesn't exist it will be created,
// including any required directories.
func (f *FileTokenStore) Put(tok *usso.SSOData) error {
	data, err := json.Marshal(tok)
	if err != nil {
		return errgo.Notef(err, "cannot marshal token")
	}
	dir := filepath.Dir(f.path)
	if err := os.MkdirAll(dir, 0700); err != nil {
		return errgo.Notef(err, "cannot create directory %q", dir)
	}
	if err := ioutil.WriteFile(f.path, data, 0600); err != nil {
		return errgo.Notef(err, "cannot write file")
	}
	return nil
}

// Get implements TokenStore.Get by
// reading the token from the FileTokenStore's file.
func (f *FileTokenStore) Get() (*usso.SSOData, error) {
	data, err := ioutil.ReadFile(f.path)
	if err != nil {
		return nil, errgo.Notef(err, "cannot read token")
	}
	var tok usso.SSOData
	if err := json.Unmarshal(data, &tok); err != nil {
		return nil, errgo.Notef(err, "cannot unmarshal token")
	}
	return &tok, nil
}

// isUSSOError determines if err represents an error of type *usso.Error.
func isUSSOError(err error) bool {
	_, ok := err.(*usso.Error)
	return ok
}