Device authorization for Microsoft Azure
Posted: ,
Modified:
Summary
We need an OAuth access token to access APIs of Microsoft Azure. Azure provides several ways to obtain access tokens but this blog post focuses to device authorization, which is used in Azure’s CLI command.
Register application
We need a client ID to run device authorization protocol. Those information is issued from Azure’s portal after registering our application.
“App registrations” is in Security + Identity section.
Click “New application registration” button and type some application name;
and choose native in application type.
Sign-on is also required but we don’t use it and any random URL,
such as http://localhost:18230
, is fine.
After the registration has been success, we can fine a client ID as an application ID.
Get a device code
In the device authentication protocol, at first, we need to get a device code by requesting https://login.microsoftonline.com/common/oauth2/devicecode with the client ID and resource names to be used in the application.
The response of the request has the following structure:
type DeviceCode struct {
UserCode string `json:"user_code"`
DeviceCode string `json:"device_code"`
VerificationURL string `json:"verification_url"`
ExpiresIn string `json:"expires_in"`
Interval string `json:"interval"`
Message string `json:"message"`
}
and the following function requests a device code and returns an object in the above structure:
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"golang.org/x/net/context/ctxhttp"
)
func GetDeviceCode(ctx context.Context, clientID string) (code *DeviceCode, err error) {
u := fmt.Sprintf(
"https://login.microsoftonline.com/common/oauth2/devicecode?client_id=%v&resource=%v",
clientID,
url.QueryEscape("https://management.core.windows.net/"))
req, err := http.NewRequest("Get", u, nil)
if err != nil {
return
}
req.Header.Add("Accept", "application/json")
res, err := ctxhttp.Do(ctx, nil, req)
if err != nil {
return
}
defer res.Body.Close()
code = new(DeviceCode)
err = json.NewDecoder(res.Body).Decode(&code)
return
}
Get an access token
Next, we need to ask users to open VerificationURL
and input UserCode
in their browser.
After users finish this process, we can get an access token from
https://login.microsoftonline.com/common/oauth2/token.
In other words, we need to poll the above URL until uses finishes to access
the verification URL.
Note that, the polling request needs to include the client ID and the device
code.
An successful response of the polling has the following structure:
type Token struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn string `json:"expires_in"`
ExpiresOn string `json:"expires_on"`
Resource string `json:"resource"`
Scope string `json:"scope"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token"`
}
on the other hand, if the polling request has been failed, the following structured message will be returned:
type TokenError struct {
ErrorSummary string `json:"error"`
ErrorDescription string `json:"error_description"`
ErrorCodes []int `json:"error_codes"`
Timestamp string `json:"timestamp"`
TraceID string `json:"trace_id"`
CorrelationID string `json:"correlation_id"`
}
// Error implements interface `error` to the above structure.
func (e *TokenError) Error() string {
return e.ErrorDescription
}
The following function requests a device code and polls an access token until a user finishes authorization:
func AuthorizeDeviceCode(ctx context.Context, clientID string) (token *Token, err error) {
// Get a device code.
code, err := GetDeviceCode(ctx, clientID)
if err != nil {
return
}
// Ask users to the verification URL and input the user code.
fmt.Println(code.Message)
// ExpiresIn means when the device code is expired.
expire, err := strconv.Atoi(code.ExpiresIn)
if err != nil {
return
}
interval, err := strconv.Atoi(code.Interval)
if err != nil {
return
}
// Cancel polling request before the device code is expired via context.
ctx, cancel := context.WithDeadline(
ctx, time.Now().Add(time.Duration(expire-30)*time.Second))
defer cancel()
// Start polling.
var req *http.Request
var res *http.Response
for {
body := fmt.Sprintf(
"resource=%v&client_id=%v&grant_type=device_code&code=%v",
url.QueryEscape("https://management.core.windows.net/"),
clientID,
code.DeviceCode)
req, err = http.NewRequest("Post", "https://login.microsoftonline.com/common/oauth2/token", strings.NewReader(body))
if err != nil {
return
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Accept", "application/json")
res, err = ctxhttp.Do(ctx, nil, req)
if err != nil {
break
}
if res.StatusCode == 400 {
var autherror TokenError
err = json.NewDecoder(res.Body).Decode(&autherror)
res.Body.Close()
if err != nil {
break
}
if strings.ToLower(autherror.ErrorSummary) != "authorization_pending" {
break
}
} else {
token = new(Token)
err = json.NewDecoder(res.Body).Decode(token)
res.Body.Close()
if err != nil {
return
}
break
}
select {
case <-ctx.Done():
err = ctx.Err()
break
case <-time.After(time.Duration(interval) * time.Second):
}
}
return
}
Renew an access token
A received access token has ExpiresIn
and ExpiresOn
; and the token will be
expired in those time.
We can renew the token by RefreshToken
.
Note that, to renew the token, we need the user’s tenant ID (Active Directory’s directory ID).
The following function renews an access token:
func RefreshToken(token *Token, clientID, tenantID string) (newToken *Token, err error) {
request := make(url.Values)
request.Add("grant_type", "refresh_token")
request.Add("client_id", clientID)
request.Add("refresh_token", token.RefreshToken)
request.Add("resource", "https://management.core.windows.net/")
res, err := http.PostForm(fmt.Sprintf(tokenEndpoint, tenantID), request)
if err != nil {
return
}
defer res.Body.Close()
if res.StatusCode != 200 {
var info TokenError
err = json.NewDecoder(res.Body).Decode(&info)
if err != nil{
return nil, err
}
return nil, &info
}
newToken = new(Token)
err = json.NewDecoder(res.Body).Decode(newToken)
return
}