Skip to content

Commit 4c2aa01

Browse files
committed
Add OAuth token scope for Jamf Pro and Microsoft Graph APIs
1 parent d8f4fd2 commit 4c2aa01

File tree

6 files changed

+246
-10
lines changed

6 files changed

+246
-10
lines changed

apiintegrations/apihandler/apihandler.go

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type APIHandler interface {
1919
GetAcceptHeader() string
2020
GetDefaultBaseDomain() string
2121
GetOAuthTokenEndpoint() string
22+
GetOAuthTokenScope() string
2223
GetBearerTokenEndpoint() string
2324
GetTokenRefreshEndpoint() string
2425
GetTokenInvalidateEndpoint() string

apiintegrations/jamfpro/jamfpro_api_handler_constants.go

+6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const (
55
APIName = "jamf pro" // APIName: represents the name of the API.
66
DefaultBaseDomain = ".jamfcloud.com" // DefaultBaseDomain: represents the base domain for the jamf instance.
77
OAuthTokenEndpoint = "/api/oauth/token" // OAuthTokenEndpoint: The endpoint to obtain an OAuth token.
8+
OAuthTokenScope = "" // OAuthTokenScope: Not used for Jamf.
89
BearerTokenEndpoint = "/api/v1/auth/token" // BearerTokenEndpoint: The endpoint to obtain a bearer token.
910
TokenRefreshEndpoint = "/api/v1/auth/keep-alive" // TokenRefreshEndpoint: The endpoint to refresh an existing token.
1011
TokenInvalidateEndpoint = "/api/v1/auth/invalidate-token" // TokenInvalidateEndpoint: The endpoint to invalidate an active token.
@@ -23,6 +24,11 @@ func (j *JamfAPIHandler) GetOAuthTokenEndpoint() string {
2324
return OAuthTokenEndpoint
2425
}
2526

27+
// GetOAuthTokenScope returns the scope for the OAuth token scope
28+
func (j *JamfAPIHandler) GetOAuthTokenScope() string {
29+
return OAuthTokenScope
30+
}
31+
2632
// GetBearerTokenEndpoint returns the endpoint for obtaining a bearer token. Used for constructing API URLs for the http client.
2733
func (j *JamfAPIHandler) GetBearerTokenEndpoint() string {
2834
return BearerTokenEndpoint

apiintegrations/msgraph/msgraph_api_handler_constants.go

+15-9
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@ package msgraph
33

44
// Endpoint constants represent the URL suffixes used for graph API token interactions.
55
const (
6-
APIName = "microsoft graph" // APIName: represents the name of the API.
7-
DefaultBaseDomain = "graph.microsoft.com" // DefaultBaseDomain: represents the base domain for the graph instance.
8-
OAuthTokenEndpoint = "/oauth2/v2.0/token" // OAuthTokenEndpoint: The endpoint to obtain an OAuth token.
9-
BearerTokenEndpoint = "" // BearerTokenEndpoint: The endpoint to obtain a bearer token.
10-
TokenRefreshEndpoint = "graph.microsoft.com" // TokenRefreshEndpoint: The endpoint to refresh an existing token.
11-
TokenInvalidateEndpoint = "graph.microsoft.com" // TokenInvalidateEndpoint: The endpoint to invalidate an active token.
12-
BearerTokenAuthenticationSupport = true // BearerTokenAuthSuppport: A boolean to indicate if the API supports bearer token authentication.
13-
OAuthAuthenticationSupport = true // OAuthAuthSuppport: A boolean to indicate if the API supports OAuth authentication.
14-
OAuthWithCertAuthenticationSupport = true // OAuthWithCertAuthSuppport: A boolean to indicate if the API supports OAuth with client certificate authentication.
6+
APIName = "microsoft graph" // APIName: represents the name of the API.
7+
DefaultBaseDomain = "graph.microsoft.com" // DefaultBaseDomain: represents the base domain for the graph instance.
8+
OAuthTokenEndpoint = "/oauth2/v2.0/token" // OAuthTokenEndpoint: The endpoint to obtain an OAuth token.
9+
OAuthTokenScope = "https://graph.microsoft.com/.default" // OAuthTokenScope: The scope for the OAuth token.
10+
BearerTokenEndpoint = "graph.microsoft.com" // BearerTokenEndpoint: The endpoint to obtain a bearer token.
11+
TokenRefreshEndpoint = "graph.microsoft.com" // TokenRefreshEndpoint: The endpoint to refresh an existing token.
12+
TokenInvalidateEndpoint = "graph.microsoft.com" // TokenInvalidateEndpoint: The endpoint to invalidate an active token.
13+
BearerTokenAuthenticationSupport = true // BearerTokenAuthSuppport: A boolean to indicate if the API supports bearer token authentication.
14+
OAuthAuthenticationSupport = true // OAuthAuthSuppport: A boolean to indicate if the API supports OAuth authentication.
15+
OAuthWithCertAuthenticationSupport = true // OAuthWithCertAuthSuppport: A boolean to indicate if the API supports OAuth with client certificate authentication.
1516
)
1617

1718
// GetDefaultBaseDomain returns the default base domain used for constructing API URLs to the http client.
@@ -24,6 +25,11 @@ func (g *GraphAPIHandler) GetOAuthTokenEndpoint() string {
2425
return OAuthTokenEndpoint
2526
}
2627

28+
// GetOAuthTokenScope returns the scope for the OAuth token scope
29+
func (g *GraphAPIHandler) GetOAuthTokenScope() string {
30+
return OAuthTokenScope
31+
}
32+
2733
// GetBearerTokenEndpoint returns the endpoint for obtaining a bearer token. Used for constructing API URLs for the http client.
2834
func (g *GraphAPIHandler) GetBearerTokenEndpoint() string {
2935
return BearerTokenEndpoint
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// http_client_oauth.go
2+
/* The http_client_auth package focuses on authentication mechanisms for an HTTP client.
3+
It provides structures and methods for handling OAuth-based authentication
4+
*/
5+
package http_client
6+
7+
import (
8+
"bytes"
9+
"crypto/tls"
10+
"crypto/x509"
11+
"encoding/json"
12+
"fmt"
13+
"io"
14+
"net/http"
15+
"net/url"
16+
"os"
17+
"time"
18+
)
19+
20+
const Authority = "https://login.microsoftonline.com/"
21+
const Scope = "https://graph.microsoft.com/.default"
22+
23+
// OAuthResponse represents the response structure when obtaining an OAuth access token.
24+
type OAuthResponse struct {
25+
AccessToken string `json:"access_token"`
26+
ExpiresIn int64 `json:"expires_in"`
27+
TokenType string `json:"token_type"`
28+
RefreshToken string `json:"refresh_token,omitempty"`
29+
Error string `json:"error,omitempty"`
30+
}
31+
32+
// ObtainOauthTokenWithApp fetches an OAuth access token using client credentials.
33+
func (c *Client) ObtainOauthTokenWithApp(tenantID, clientID, clientSecret string) (*OAuthResponse, error) {
34+
endpoint := fmt.Sprintf("%s%s/oauth2/v2.0/token", Authority, tenantID)
35+
36+
data := url.Values{}
37+
data.Set("client_id", clientID)
38+
data.Set("scope", Scope)
39+
data.Set("client_secret", clientSecret)
40+
data.Set("grant_type", "client_credentials")
41+
42+
req, err := http.NewRequest("POST", endpoint, bytes.NewBufferString(data.Encode()))
43+
if err != nil {
44+
return nil, err
45+
}
46+
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
47+
48+
client := &http.Client{}
49+
resp, err := client.Do(req)
50+
if err != nil {
51+
return nil, err
52+
}
53+
defer resp.Body.Close()
54+
55+
bodyBytes, err := io.ReadAll(resp.Body)
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
// Debug: Print the entire raw response body for inspection
61+
c.logger.Debug("Raw response body: %s\n", string(bodyBytes))
62+
63+
// Create a new reader from the body bytes for json unmarshalling
64+
bodyReader := bytes.NewReader(bodyBytes)
65+
66+
oauthResp := &OAuthResponse{}
67+
err = json.NewDecoder(bodyReader).Decode(oauthResp)
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
if oauthResp.Error != "" {
73+
return nil, fmt.Errorf("error obtaining OAuth token: %s", oauthResp.Error)
74+
}
75+
76+
// Calculate and format token expiration time
77+
expiresIn := time.Duration(oauthResp.ExpiresIn) * time.Second
78+
expirationTime := time.Now().Add(expiresIn)
79+
formattedExpirationTime := expirationTime.Format(time.RFC1123)
80+
81+
// Log the token life expiry details in a human-readable format
82+
c.logger.Debug("The OAuth token obtained is: ",
83+
"Valid for", expiresIn.String(),
84+
"Expires at", formattedExpirationTime)
85+
86+
return oauthResp, nil
87+
}
88+
89+
// ObtainOauthTokenWithCertificate fetches an OAuth access token using a certificate.
90+
func (c *Client) ObtainOauthTokenWithCertificate(tenantID, clientID, thumbprint, keyFile string) (*OAuthResponse, error) {
91+
endpoint := fmt.Sprintf("%s%s/oauth2/v2.0/token", Authority, tenantID)
92+
93+
// Load the certificate
94+
certData, err := os.ReadFile(keyFile)
95+
if err != nil {
96+
return nil, fmt.Errorf("failed to read certificate file: %v", err)
97+
}
98+
99+
cert, err := tls.X509KeyPair(certData, certData)
100+
if err != nil {
101+
return nil, fmt.Errorf("failed to parse certificate: %v", err)
102+
}
103+
104+
// Create a custom HTTP client with the certificate
105+
certPool := x509.NewCertPool()
106+
certPool.AppendCertsFromPEM(certData)
107+
tlsConfig := &tls.Config{
108+
Certificates: []tls.Certificate{cert},
109+
RootCAs: certPool,
110+
InsecureSkipVerify: true, // Depending on your requirements, you might want to adjust this
111+
}
112+
client := &http.Client{
113+
Transport: &http.Transport{
114+
TLSClientConfig: tlsConfig,
115+
},
116+
}
117+
118+
// Prepare request data
119+
data := url.Values{}
120+
data.Set("client_id", clientID)
121+
data.Set("scope", Scope)
122+
data.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
123+
data.Set("client_assertion", thumbprint) // You might need to adjust this according to your requirements
124+
data.Set("grant_type", "client_credentials")
125+
126+
req, err := http.NewRequest("POST", endpoint, bytes.NewBufferString(data.Encode()))
127+
if err != nil {
128+
return nil, err
129+
}
130+
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
131+
132+
resp, err := client.Do(req)
133+
if err != nil {
134+
return nil, err
135+
}
136+
defer resp.Body.Close()
137+
138+
bodyBytes, err := io.ReadAll(resp.Body)
139+
if err != nil {
140+
return nil, err
141+
}
142+
143+
// Debug: Print the entire raw response body for inspection
144+
c.logger.Debug("Raw response body: %s\n", string(bodyBytes))
145+
146+
bodyReader := bytes.NewReader(bodyBytes)
147+
oauthResp := &OAuthResponse{}
148+
err = json.NewDecoder(bodyReader).Decode(oauthResp)
149+
if err != nil {
150+
return nil, err
151+
}
152+
153+
if oauthResp.Error != "" {
154+
return nil, fmt.Errorf("error obtaining OAuth token: %s", oauthResp.Error)
155+
}
156+
157+
expiresIn := time.Duration(oauthResp.ExpiresIn) * time.Second
158+
expirationTime := time.Now().Add(expiresIn)
159+
formattedExpirationTime := expirationTime.Format(time.RFC1123)
160+
c.logger.Debug("The OAuth token obtained is: ",
161+
"Valid for", expiresIn.String(),
162+
"Expires at", formattedExpirationTime)
163+
164+
return oauthResp, nil
165+
}
166+
167+
// GetOAuthCredentials retrieves the current OAuth credentials (Client ID and Client Secret)
168+
// set for the client instance. Used for test cases.
169+
func (c *Client) GetOAuthCredentials() OAuthCredentials {
170+
return c.OAuthCredentials
171+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// http_client_auth_token_management.go
2+
package http_client
3+
4+
import (
5+
"fmt"
6+
"time"
7+
)
8+
9+
// TokenResponse represents the structure of a token response from the API.
10+
type TokenResponse struct {
11+
Token string `json:"token"`
12+
Expires time.Time `json:"expires"`
13+
}
14+
15+
// ValidAuthTokenCheck checks if the current token is valid and not close to expiry.
16+
// If the token is invalid or close to expiry, it tries to obtain a new token.
17+
func (c *Client) ValidAuthTokenCheck() (bool, error) {
18+
if c.Token == "" || time.Until(c.Expiry) < c.config.TokenRefreshBufferPeriod {
19+
var oauthResp *OAuthResponse
20+
var err error
21+
22+
switch c.AuthMethod {
23+
case "oauthApp":
24+
// Obtain token using OAuth App credentials
25+
oauthResp, err = c.ObtainOauthTokenWithApp(c.TenantID, c.OAuthCredentials.ClientID, c.OAuthCredentials.ClientSecret)
26+
27+
case "oauthCertificate":
28+
// Obtain token using OAuth Certificate credentials
29+
oauthResp, err = c.ObtainOauthTokenWithCertificate(c.TenantID, c.OAuthCredentials.ClientID, c.OAuthCredentials.CertThumbprint, c.OAuthCredentials.CertificatePath)
30+
31+
default:
32+
return false, fmt.Errorf("unknown auth method: %s", c.AuthMethod)
33+
}
34+
35+
if err != nil {
36+
return false, fmt.Errorf("failed to obtain new token: %w", err)
37+
}
38+
39+
// Update the token and expiry time if a new token was obtained
40+
if oauthResp != nil {
41+
c.Token = oauthResp.AccessToken
42+
expiresIn := time.Duration(oauthResp.ExpiresIn) * time.Second
43+
c.Expiry = time.Now().Add(expiresIn)
44+
}
45+
}
46+
47+
return true, nil
48+
}

authenticationhandler/auth_oauth.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,19 @@ type OAuthResponse struct {
3333
// ObtainOAuthToken fetches an OAuth access token using the provided client ID and client secret.
3434
// It updates the AuthTokenHandler's Token and Expires fields with the obtained values.
3535
func (h *AuthTokenHandler) ObtainOAuthToken(apiHandler apihandler.APIHandler, httpClient *http.Client, clientID, clientSecret string) error {
36-
// Use the APIHandler's method to get the OAuth token endpoint
36+
// Get the OAuth token endpoint from the APIHandler
3737
oauthTokenEndpoint := apiHandler.GetOAuthTokenEndpoint()
3838

3939
// Construct the full authentication endpoint URL
4040
authenticationEndpoint := apiHandler.ConstructAPIAuthEndpoint(h.InstanceName, oauthTokenEndpoint, h.Logger)
4141

42+
// Get the OAuth token scope from the APIHandler
43+
oauthTokenScope := apiHandler.GetOAuthTokenScope()
44+
4245
data := url.Values{}
4346
data.Set("client_id", clientID)
4447
data.Set("client_secret", clientSecret)
48+
data.Set("scope", oauthTokenScope)
4549
data.Set("grant_type", "client_credentials")
4650

4751
h.Logger.Debug("Attempting to obtain OAuth token", zap.String("ClientID", clientID))

0 commit comments

Comments
 (0)