diff options
Diffstat (limited to 'vendor/github.com/yaegashi/msgraph.go/msauth')
4 files changed, 341 insertions, 0 deletions
diff --git a/vendor/github.com/yaegashi/msgraph.go/msauth/README.md b/vendor/github.com/yaegashi/msgraph.go/msauth/README.md new file mode 100644 index 00000000..43aead20 --- /dev/null +++ b/vendor/github.com/yaegashi/msgraph.go/msauth/README.md @@ -0,0 +1,70 @@ +# msauth + +## Introduction + +Very simple package to authorize applications against [Microsoft identity platform]. + +It utilizes [v2.0 endpoint] so that it can authorize users using both personal (Microsoft) and organizational (Azure AD) account. + +## Usage + +### Device authorization grant + +- [OAuth 2.0 device authorization grant flow] + +```go +const ( + tenantID = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + clientID = "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY" + tokenCachePath = "token_cache.json" +) + +var scopes = []string{"openid", "profile", "offline_access", "User.Read", "Files.Read"} + + ctx := context.Background() + m := msauth.NewManager() + m.LoadFile(tokenCachePath) + ts, err := m.DeviceAuthorizationGrant(ctx, tenantID, clientID, scopes, nil) + if err != nil { + log.Fatal(err) + } + m.SaveFile(tokenCachePath) + + httpClient := oauth2.NewClient(ctx, ts) + ... +``` + +### Client credentials grant + +- [OAuth 2.0 client credentials grant flow] + +```go +const ( + tenantID = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + clientID = "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY" + clientSecret = "ZZZZZZZZZZZZZZZZZZZZZZZZ" +) + +var scopes = []string{msauth.DefaultMSGraphScope} + + ctx := context.Background() + m := msauth.NewManager() + ts, err := m.ClientCredentialsGrant(ctx, tenantID, clientID, clientSecret, scopes) + if err != nil { + log.Fatal(err) + } + + httpClient := oauth2.NewClient(ctx, ts) + ... +``` + +### Authorization code grant + +- [OAuth 2.0 authorization code grant flow] +- Not yet implemented. + +[Microsoft identity platform]: https://docs.microsoft.com/en-us/azure/active-directory/develop/ +[v2.0 endpoint]: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-overview +[OAuth 2.0 device authorization grant flow]: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code +[OAuth 2.0 client credentials grant flow]: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow +[OAuth 2.0 authorization code grant flow]: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
\ No newline at end of file diff --git a/vendor/github.com/yaegashi/msgraph.go/msauth/client_credentials_grant.go b/vendor/github.com/yaegashi/msgraph.go/msauth/client_credentials_grant.go new file mode 100644 index 00000000..bc50883d --- /dev/null +++ b/vendor/github.com/yaegashi/msgraph.go/msauth/client_credentials_grant.go @@ -0,0 +1,27 @@ +package msauth + +import ( + "context" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" + "golang.org/x/oauth2/microsoft" +) + +// ClientCredentialsGrant performs OAuth 2.0 client credentials grant and returns auto-refreshing TokenSource +func (m *Manager) ClientCredentialsGrant(ctx context.Context, tenantID, clientID, clientSecret string, scopes []string) (oauth2.TokenSource, error) { + config := &clientcredentials.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + TokenURL: microsoft.AzureADEndpoint(tenantID).TokenURL, + Scopes: scopes, + AuthStyle: oauth2.AuthStyleInParams, + } + var err error + ts := config.TokenSource(ctx) + _, err = ts.Token() + if err != nil { + return nil, err + } + return ts, nil +} diff --git a/vendor/github.com/yaegashi/msgraph.go/msauth/device_authorization_grant.go b/vendor/github.com/yaegashi/msgraph.go/msauth/device_authorization_grant.go new file mode 100644 index 00000000..4baafd8d --- /dev/null +++ b/vendor/github.com/yaegashi/msgraph.go/msauth/device_authorization_grant.go @@ -0,0 +1,96 @@ +package msauth + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + "time" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/microsoft" +) + +const ( + deviceCodeGrantType = "urn:ietf:params:oauth:grant-type:device_code" + authorizationPendingError = "authorization_pending" +) + +// DeviceCode is returned on device auth initiation +type DeviceCode struct { + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURL string `json:"verification_url"` + ExpiresIn int `json:"expires_in"` + Interval int `json:"interval"` + Message string `json:"message"` +} + +// DeviceAuthorizationGrant performs OAuth 2.0 device authorization grant and returns auto-refreshing TokenSource +func (m *Manager) DeviceAuthorizationGrant(ctx context.Context, tenantID, clientID string, scopes []string, callback func(*DeviceCode) error) (oauth2.TokenSource, error) { + endpoint := microsoft.AzureADEndpoint(tenantID) + endpoint.AuthStyle = oauth2.AuthStyleInParams + config := &oauth2.Config{ + ClientID: clientID, + Endpoint: endpoint, + Scopes: scopes, + } + if t, ok := m.TokenCache[generateKey(tenantID, clientID)]; ok { + tt, err := config.TokenSource(ctx, t).Token() + if err == nil { + return config.TokenSource(ctx, tt), nil + } + if _, ok := err.(*oauth2.RetrieveError); !ok { + return nil, err + } + } + scope := strings.Join(scopes, " ") + res, err := http.PostForm(deviceCodeURL(tenantID), url.Values{"client_id": {clientID}, "scope": {scope}}) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + b, _ := ioutil.ReadAll(res.Body) + return nil, fmt.Errorf("%s: %s", res.Status, string(b)) + } + dc := &DeviceCode{} + dec := json.NewDecoder(res.Body) + err = dec.Decode(&dc) + if err != nil { + return nil, err + } + if callback != nil { + err = callback(dc) + if err != nil { + return nil, err + } + } else { + fmt.Fprintln(os.Stderr, dc.Message) + } + values := url.Values{ + "client_id": {clientID}, + "grant_type": {deviceCodeGrantType}, + "device_code": {dc.DeviceCode}, + } + interval := dc.Interval + if interval == 0 { + interval = 5 + } + for { + time.Sleep(time.Second * time.Duration(interval)) + token, err := m.requestToken(ctx, tenantID, clientID, values) + if err == nil { + m.Cache(tenantID, clientID, token) + return config.TokenSource(ctx, token), nil + } + tokenError, ok := err.(*TokenError) + if !ok || tokenError.ErrorObject != authorizationPendingError { + return nil, err + } + } +} diff --git a/vendor/github.com/yaegashi/msgraph.go/msauth/msauth.go b/vendor/github.com/yaegashi/msgraph.go/msauth/msauth.go new file mode 100644 index 00000000..c2a86985 --- /dev/null +++ b/vendor/github.com/yaegashi/msgraph.go/msauth/msauth.go @@ -0,0 +1,148 @@ +// Package msauth implements a library to authorize against Microsoft identity platform: +// https://docs.microsoft.com/en-us/azure/active-directory/develop/ +// +// It utilizes v2.0 endpoint +// so it can authorize users with both personal (Microsoft) and organizational (Azure AD) account. +package msauth + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "sync" + "time" + + "golang.org/x/oauth2" +) + +const ( + // DefaultMSGraphScope is the default scope for MS Graph API + DefaultMSGraphScope = "https://graph.microsoft.com/.default" + endpointURLFormat = "https://login.microsoftonline.com/%s/oauth2/v2.0/%s" +) + +// TokenError is returned on failed authentication +type TokenError struct { + ErrorObject string `json:"error"` + ErrorDescription string `json:"error_description"` +} + +// Error implements error interface +func (t *TokenError) Error() string { + return fmt.Sprintf("%s: %s", t.ErrorObject, t.ErrorDescription) +} + +func generateKey(tenantID, clientID string) string { + return fmt.Sprintf("%s:%s", tenantID, clientID) +} + +func deviceCodeURL(tenantID string) string { + return fmt.Sprintf(endpointURLFormat, tenantID, "devicecode") +} + +func tokenURL(tenantID string) string { + return fmt.Sprintf(endpointURLFormat, tenantID, "token") +} + +type tokenJSON struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` +} + +func (e *tokenJSON) expiry() (t time.Time) { + if v := e.ExpiresIn; v != 0 { + return time.Now().Add(time.Duration(v) * time.Second) + } + return +} + +// Manager is oauth2 token cache manager +type Manager struct { + mu sync.Mutex + TokenCache map[string]*oauth2.Token +} + +// NewManager returns a new Manager instance +func NewManager() *Manager { + return &Manager{TokenCache: map[string]*oauth2.Token{}} +} + +// LoadBytes loads token cache from opaque bytes (it's actually JSON) +func (m *Manager) LoadBytes(b []byte) error { + m.mu.Lock() + defer m.mu.Unlock() + return json.Unmarshal(b, &m.TokenCache) +} + +// SaveBytes saves token cache to opaque bytes (it's actually JSON) +func (m *Manager) SaveBytes() ([]byte, error) { + m.mu.Lock() + defer m.mu.Unlock() + return json.Marshal(m.TokenCache) +} + +// LoadFile loads token cache from file +func (m *Manager) LoadFile(path string) error { + b, err := ioutil.ReadFile(path) + if err != nil { + return err + } + return m.LoadBytes(b) +} + +// SaveFile saves token cache to file +func (m *Manager) SaveFile(path string) error { + b, err := m.SaveBytes() + if err != nil { + return err + } + return ioutil.WriteFile(path, b, 0644) +} + +// Cache stores a token into token cache +func (m *Manager) Cache(tenantID, clientID string, token *oauth2.Token) { + m.TokenCache[generateKey(tenantID, clientID)] = token +} + +// requestToken requests a token from the token endpoint +// TODO(ctx): use http client from ctx +func (m *Manager) requestToken(ctx context.Context, tenantID, clientID string, values url.Values) (*oauth2.Token, error) { + res, err := http.PostForm(tokenURL(tenantID), values) + if err != nil { + return nil, err + } + defer res.Body.Close() + b, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + if res.StatusCode != http.StatusOK { + var terr *TokenError + err = json.Unmarshal(b, &terr) + if err != nil { + return nil, err + } + return nil, terr + } + var tj *tokenJSON + err = json.Unmarshal(b, &tj) + if err != nil { + return nil, err + } + token := &oauth2.Token{ + AccessToken: tj.AccessToken, + TokenType: tj.TokenType, + RefreshToken: tj.RefreshToken, + Expiry: tj.expiry(), + } + if token.AccessToken == "" { + return nil, errors.New("msauth: server response missing access_token") + } + return token, nil +} |