Skip to content

Commit

Permalink
Move oauth implementation into internal package, add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
manicminer committed Jan 13, 2021
1 parent 0e7cca8 commit 01a2f92
Show file tree
Hide file tree
Showing 27 changed files with 239 additions and 19 deletions.
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,57 @@
# Hamilton is a Go SDK for Microsoft Graph

This is an active work in progress and highly subject to change.
This is a working Go client for the [Microsoft Graph API][ms-graph-docs]. It is actively maintained and has growing support for services and objects in Azure Active Directory.

Do not use.
## Example Usage

```go
package example

import (
"context"
"fmt"
"log"

"github.com/manicminer/hamilton/auth"
"github.com/manicminer/hamilton/clients"
)

const (
tenantId = "00000000-0000-0000-0000-000000000000"
clientId = "11111111-1111-1111-1111-111111111111"
clientSecret = "My$3cR3t"
)

func main() {
ctx := context.Background()

authConfig := &auth.Config{
TenantID: tenantId,
ClientID: clientId,
ClientSecret: clientSecret,
EnableClientSecretAuth: true,
}

authorizer, err := authConfig.NewAuthorizer(ctx)
if err != nil {
log.Fatal(err)
}

client := clients.NewUsersClient(tenantId)
client.BaseClient.Authorizer = authorizer

users, _, err := client.List(ctx, "")
if err != nil {
log.Fatal(err)
}
if users == nil {
log.Fatalln("bad API response, nil result received")
}

for _, user := range *users {
fmt.Printf("%s: %s <%s>\n", *user.ID, *user.DisplayName, *user.Mail)
}
}
```

[ms-graph-docs]: https://docs.microsoft.com/en-us/graph/overview
47 changes: 36 additions & 11 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,59 @@ import (
"io/ioutil"
"strings"

microsoft2 "github.com/manicminer/hamilton/auth/microsoft"
microsoft2 "github.com/manicminer/hamilton/auth/internal/microsoft"
)

type Config struct {
//Environment string
// Azure Active Directory tenant to connect to, should be a valid UUID
TenantID string

// Client ID for the application used to authenticate the connection
ClientID string

// Azure CLI Tokens Auth
// Enables authentication using Azure CLI
EnableAzureCliToken bool

// Managed Service Identity Auth
// Enables authentication using managed service identity. Not yet supported.
// TODO: NOT YET SUPPORTED
EnableMsiAuth bool
MsiEndpoint string

// Service Principal (Client Cert) Auth
// Specifies a custom MSI endpoint to connect to
MsiEndpoint string

// Enables client certificate authentication using client assertions
EnableClientCertAuth bool
ClientCertPath string
ClientCertPassword string

// Service Principal (Client Secret) Auth
// Specifies the path to a client certificate bundle in PFX format
ClientCertPath string

// Specifies the encryption password to unlock a client certificate
ClientCertPassword string

// Enables client secret authentication using client credentials
EnableClientSecretAuth bool
ClientSecret string
ClientSecretDocsLink string

// Specifies the password to authenticate with using client secret authentication
ClientSecret string
}

// Authorizer is anything that can return an access token for authorizing API connections
type Authorizer interface {
Token() (*oauth2.Token, error)
}

// NewAuthorizer returns a suitable Authorizer depending on what is defined in the Config
// Authorizers are selected for authentication methods in the following preferential order:
// - Client certificate authentication
// - Client secret authentication
// - Azure CLI authentication
//
// Whether one of these is returned depends on whether it is enabled in the Config, and whether sufficient
// configuration fields are set to enable that authentication method.
//
// For client certificate authentication, specify TenantID, ClientID and ClientCertPath.
// For client secret authentication, specify TenantID, ClientID and ClientSecret.
// Azure CLI authentication (if enabled) is used as a fallback mechanism.
func (c *Config) NewAuthorizer(ctx context.Context) (Authorizer, error) {
if c.EnableClientCertAuth && strings.TrimSpace(c.TenantID) != "" && strings.TrimSpace(c.ClientID) != "" && strings.TrimSpace(c.ClientCertPath) != "" {
a, err := NewClientCertificateAuthorizer(ctx, c.TenantID, c.ClientID, c.ClientCertPath, c.ClientCertPassword)
Expand Down Expand Up @@ -78,6 +100,7 @@ func (c *Config) NewAuthorizer(ctx context.Context) (Authorizer, error) {
return nil, fmt.Errorf("no Authorizer could be configured, please check your configuration")
}

// NewAzureCliAuthorizer returns an Authorizer which authenticates using the Azure CLI.
func NewAzureCliAuthorizer(ctx context.Context, tenantId string) (Authorizer, error) {
conf, err := NewAzureCliConfig(tenantId)
if err != nil {
Expand All @@ -86,6 +109,7 @@ func NewAzureCliAuthorizer(ctx context.Context, tenantId string) (Authorizer, er
return conf.TokenSource(ctx), nil
}

// NewClientCertificateAuthorizer returns an authorizer which uses client certificate authentication.
func NewClientCertificateAuthorizer(ctx context.Context, tenantId, clientId, pfxPath, pfxPass string) (Authorizer, error) {
pfx, err := ioutil.ReadFile(pfxPath)
if err != nil {
Expand All @@ -112,6 +136,7 @@ func NewClientCertificateAuthorizer(ctx context.Context, tenantId, clientId, pfx
return conf.TokenSource(ctx), nil
}

// NewClientSecretAuthorizer returns an authorizer which uses client secret authentication.
func NewClientSecretAuthorizer(ctx context.Context, tenantId, clientId, clientSecret string) (Authorizer, error) {
conf := clientcredentials.Config{
AuthStyle: oauth2.AuthStyleInParams,
Expand Down
7 changes: 7 additions & 0 deletions auth/azcli.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ import (
"time"
)

// AzureCliAuthorizer is an Authorizer which supports the Azure CLI.
type AzureCliAuthorizer struct {
// TenantID is optional and forces selection of the specified tenant. Must be a valid UUID.
TenantID string

ctx context.Context
conf *AzureCliConfig
}

// Token returns an access token using the Azure CLI as an authentication mechanism.
func (a AzureCliAuthorizer) Token() (*oauth2.Token, error) {
// We don't need to handle token caching and refreshing since az-cli does that for us
var token struct {
Expand All @@ -40,10 +43,12 @@ func (a AzureCliAuthorizer) Token() (*oauth2.Token, error) {
}, nil
}

// AzureCliConfig configures an AzureCliAuthorizer.
type AzureCliConfig struct {
TenantID string
}

// NewAzureCliConfig validates the supplied tenant ID and returns a new AzureCliConfig.
func NewAzureCliConfig(tenantId string) (*AzureCliConfig, error) {
// check az-cli version

Expand All @@ -69,6 +74,7 @@ func NewAzureCliConfig(tenantId string) (*AzureCliConfig, error) {
return &AzureCliConfig{TenantID: tenantId}, nil
}

// TokenSource provides a source for obtaining access tokens using AzureCliAuthorizer.
func (c *AzureCliConfig) TokenSource(ctx context.Context) Authorizer {
return &AzureCliAuthorizer{
TenantID: c.TenantID,
Expand All @@ -77,6 +83,7 @@ func (c *AzureCliConfig) TokenSource(ctx context.Context) Authorizer {
}
}

// jsonUnmarshalAzCmd executes an Azure CLI command and unmarshals the JSON output.
func jsonUnmarshalAzCmd(i interface{}, arg ...string) error {
var stderr bytes.Buffer
var stdout bytes.Buffer
Expand Down
2 changes: 2 additions & 0 deletions auth/claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
)

// Claims is used to unmarshall the claims from a JWT issued by the Microsoft Identity Platform.
type Claims struct {
Audience string `json:"aud"`
Issuer string `json:"iss"`
Expand All @@ -23,6 +24,7 @@ type Claims struct {
IdType string `json:"idtyp,omitempty"`
}

// ParseClaims retrieves and parses the claims from a JWT issued by the Microsoft Identity Platform.
func ParseClaims(token *oauth2.Token) (claims Claims, err error) {
if token == nil {
return
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
"strings"
"time"

"github.com/manicminer/hamilton/auth/microsoft/internal"
"github.com/manicminer/hamilton/auth/internal/microsoft/internal"
)

// Config is the configuration for using client credentials flow with a client assertion.
Expand Down
29 changes: 24 additions & 5 deletions base/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,57 @@ const (
VersionBeta = "beta"
)

// ValidStatusFunc is a function that tests whether an HTTP response is considered valid for the particular request.
type ValidStatusFunc func(response *http.Response) bool

// HttpRequestInput is any type that can validate the response to an HTTP request.
type HttpRequestInput interface {
GetValidStatusCodes() []int
GetValidStatusFunc() ValidStatusFunc
}

// Uri represents a Microsoft Graph endpoint.
type Uri struct {
Entity string
Params url.Values
HasTenantId bool
}

// GraphClient is any suitable HTTP client.
type GraphClient = *http.Client

// Client is a base client to be used by clients for specific entities.
// It can send GET, POST, PUT, PATCH and DELETE requests to Microsoft Graph and is API version and tenant aware.
type Client struct {
// ApiVersion is the Microsoft Graph API version to use.
ApiVersion string
Endpoint string
TenantId string
UserAgent string

// Endpoint is the base endpoint for Microsoft Graph, usually "https://graph.microsoft.com".
Endpoint string

// TenantId is the tenant ID to use in requests.
TenantId string

// UserAgent is the HTTP user agent string to send in requests.
UserAgent string

// Authorizer is anything that can provide an access token with which to authorize requests.
Authorizer auth.Authorizer

httpClient GraphClient
}

func NewClient(endpoint, tenantId, version string) Client {
// NewClient returns a new Client configured with the specified endpoint, tenant ID and API version.
func NewClient(endpoint, tenantId, apiVersion string) Client {
return Client{
httpClient: http.DefaultClient,
Endpoint: endpoint,
TenantId: tenantId,
ApiVersion: version,
ApiVersion: apiVersion,
}
}

// buildUri is used by the package to build a complete URI string for API requests.
func (c Client) buildUri(uri Uri) (string, error) {
url, err := url.Parse(c.Endpoint)
if err != nil {
Expand All @@ -66,6 +83,7 @@ func (c Client) buildUri(uri Uri) (string, error) {
return url.String(), nil
}

// performRequest is used by the package to send an HTTP request to the API.
func (c Client) performRequest(req *http.Request, input HttpRequestInput) (*http.Response, int, error) {
var status int

Expand Down Expand Up @@ -104,6 +122,7 @@ func (c Client) performRequest(req *http.Request, input HttpRequestInput) (*http
return resp, status, nil
}

// containsStatusCode determines whether the returned status code is in the []int of expected status codes.
func containsStatusCode(expected []int, actual int) bool {
for _, v := range expected {
if actual == v {
Expand Down
4 changes: 4 additions & 0 deletions base/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@ import (
"net/http"
)

// DeleteHttpRequestInput configures a DELETE request.
type DeleteHttpRequestInput struct {
ValidStatusCodes []int
ValidStatusFunc ValidStatusFunc
Uri Uri
}

// GetValidStatusCodes returns a []int of status codes considered valid for a DELETE request.
func (i DeleteHttpRequestInput) GetValidStatusCodes() []int {
return i.ValidStatusCodes
}

// GetValidStatusFunc returns a function used to evaluate whether the response to a DELETE request is considered valid.
func (i DeleteHttpRequestInput) GetValidStatusFunc() ValidStatusFunc {
return i.ValidStatusFunc
}

// Delete performs a DELETE request.
func (c Client) Delete(ctx context.Context, input DeleteHttpRequestInput) (*http.Response, int, error) {
var status int
url, err := c.buildUri(input.Uri)
Expand Down
4 changes: 4 additions & 0 deletions base/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@ import (
"net/http"
)

// GetHttpRequestInput configures a GET request.
type GetHttpRequestInput struct {
ValidStatusCodes []int
ValidStatusFunc ValidStatusFunc
Uri Uri
}

// GetValidStatusCodes returns a []int of status codes considered valid for a GET request.
func (i GetHttpRequestInput) GetValidStatusCodes() []int {
return i.ValidStatusCodes
}

// GetValidStatusFunc returns a function used to evaluate whether the response to a GET request is considered valid.
func (i GetHttpRequestInput) GetValidStatusFunc() ValidStatusFunc {
return i.ValidStatusFunc
}

// Get performs a GET request.
func (c Client) Get(ctx context.Context, input GetHttpRequestInput) (*http.Response, int, error) {
var status int
url, err := c.buildUri(input.Uri)
Expand Down
4 changes: 4 additions & 0 deletions base/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,25 @@ import (
"net/http"
)

// PatchHttpRequestInput configures a PATCH request.
type PatchHttpRequestInput struct {
Body []byte
ValidStatusCodes []int
ValidStatusFunc ValidStatusFunc
Uri Uri
}

// GetValidStatusCodes returns a []int of status codes considered valid for a PATCH request.
func (i PatchHttpRequestInput) GetValidStatusCodes() []int {
return i.ValidStatusCodes
}

// GetValidStatusFunc returns a function used to evaluate whether the response to a PATCH request is considered valid.
func (i PatchHttpRequestInput) GetValidStatusFunc() ValidStatusFunc {
return i.ValidStatusFunc
}

// Patch performs a PATCH request.
func (c Client) Patch(ctx context.Context, input PatchHttpRequestInput) (*http.Response, int, error) {
var status int
url, err := c.buildUri(input.Uri)
Expand Down
Loading

0 comments on commit 01a2f92

Please sign in to comment.