Using Azure Active Directory Delegated Endpoints from an Unattended Service

Maarten Sundman
Slalom Technology
Published in
6 min readDec 4, 2020

When building solutions against an Azure Active Directory protected API, you oftentimes will need to have the application run headless, unattended or non-interactive. Like as a service job, or a console application. To solve this, people oftentimes use Application API authorization, which some endpoints support. But what about when you are working with an API that only supports Delegated authorization, such as the Yammer user_impersonation endpoint? Or when you need to ensure that requests are tied to the executing user? To solve that, we have the Device Code authorization flow.

The Device Code flow is an OAuth authorization flow that has the user authorize the requesting application from a separate interface than the one which the application is running on. The generated token is also infinitely renewable allowing for daemon or service style scenarios. This method works for any Delegated endpoint protected by Azure AD, it is not specific to Yammer. You likely have already experienced what the Device Code flow is like when setting up IoT devices. For example, when your TV asks you to put in a code to access Netflix, Hulu or your other services; that’s the Device Code flow.

From a security standpoint your InfoSec team will also like the Device Code flow more, as it forces requests by your application to run under the context of the authorizing user. Allowing you to enforce who can run the application. Whereas Application authorization has the same execution context regardless of end user, as the app is already authorized.

Device Code Auth/Challenge/Verify Flow

So, how do we do all this?

We start by requesting a new Device Code. By sending a POST to https://login.microsoftonline.com/TENANT/oauth2/v2.0/devicecode with a x-www-form-urlencoded body that has the parameters:

The code for the end user to input can be exposed via a web service, command line, or just to a text file. The user who is authorizing the application, will need this code to finish authorizing the application to work on their behalf.

Request New Device Code

private async Task<DeviceCodeResponse> RequestDeviceCode(HttpClient client, string scope, bool GraphToken) {
var body = (GraphToken && String.IsNullOrBlank(scope)) ? new Dictionary<string, string>
{
["resource"] = "https://graph.microsoft.com/.default",
["client_id"] = clientId,
["grant_type"] = "device_code"
} : new Dictionary<string, string> {
["scope"] = scope,
["client_id"] = clientId,
["grant_type"] = "device_code"
};
FormUrlEncodedContent data = new FormUrlEncodedContent(body);
HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, deviceCodeUri);
req.Content = data;
Task<HttpResponseMessage> res = client.SendAsync(req);
string jsonResult = await (await res).Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<DeviceCodeResponse>(jsonResult);
}
Requesting a Device Code from PostMan

You let the user know the user_code and that they need to go to https://microsoft.com/devicelogin to put that code in to authorize the application. In the background you’ll be using the device_code to see if the user has authorized your application. The interval parameter is a suggestion for the number of seconds to wait between checks. The user gets 15 minutes (900 seconds) to authorize before the request will time out.

So next, you’ll need to check if the user has authorized your application.

Poll For Completion

Until the request expires, or until the user authorizes the app, you’ll want to send a POST to https://login.microsoftonline.com/TENANTNAME/oauth2/token with the parameters:

  • grant_type: device_code
  • resource: “https://api.yammer.com”
  • code: device_code
  • client_id: Your App’s Client ID
Completing a Token via PostMan
private async Task<TokenResponse> PollForAuthCompletion(DeviceCodeResponse request, HttpClient client) {
TimeSpan pollingInterval = TimeSpan.FromSeconds(request.interval);
DateTimeOffset codeExpiresOn = DateTimeOffset.UtcNow.AddSeconds(request.expires_in);
TimeSpan timeRemaining = codeExpiresOn - DateTimeOffset.UtcNow;
string tokenUri = "https://login.microsoftonline.com/TENANTNAME/oauth2/token"while (timeRemaining.TotalSeconds > 0) {
await Task.Delay(request.interval * 1000);
var res = await CheckTokenAvailable(tokenUri, request, client);
if (res != null)
return res;
timeRemaining = codeExpiresOn - DateTimeOffset.UtcNow;
}
return null;
}

Get Token / Check if Token is Available

private async Task<TokenResponse> CheckTokenAvailable(string tokenUri, DeviceCodeResponse request, HttpClient client) {var body = new Dictionary<string, string> {
["grant_type"] = "device_code",
["resource"] = (GraphToken) ? "https://graph.microsoft.com/.default" : "https://api.yammer.com",
["code"] = request.device_code,
["client_id"] = clientId
};
var req = new HttpRequestMessage(HttpMethod.Post, tokenUri) {
Content = new FormUrlEncodedContent(body)
};
var res = await client.SendAsync(req);
string jsonResult = await res.Content.ReadAsStringAsync();
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(jsonResult);
if (res.StatusCode == HttpStatusCode.OK) {
if (tokenResponse.error != null && tokenResponse.error != "authorization_pending") {
throw new Exception("Token acquisition failed: " + tokenResponse.error);
} else {
return tokenResponse;
}
}
return null;
}

Token

public class TokenResponse : ErrorResponse {public string token_type { get; set; }
public string scope { get; set; }
public int expires_in { get; set; }
public long expires_on { get; set; }
public long not_before { get; set; }
public string resource { get; set; }
public string access_token { get; set; }
public string refresh_token { get; set; }
public string id_token { get; set; }
}
public class ErrorResponse {
public string error { get; set; }
public string error_description { get; set; }
public int[] error_codes { get; set; }
public DateTime timestamp { get; set; }
public Guid trace_id { get; set; }
public Guid correlation_id { get; set; }
}

The ‘access_token’ is the Bearer token you’ll use in any requests. So Success! Your app can now make unattended requests to the delegated endpoints. But what about when this token expires? To handle that we need to talk about Refreshing the token (don’t worry, its all still unattended from this point forward, no need to bother someone to authorize your application again). You’ll notice a few other attributes you’ll need to keep track of as well. Specifically refresh_token, expires_on and not_before are what we’ll be using.

Note that the timestamps for expires_on and not_before are in UNIX Epoch format.

Epoch to DateTime

So lets convert that UNIX Epoch time to DateTime so our lives are easier.

private DateTime FromEpoch(long epoch) {
DateTime basetime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
return basetime.AddSeconds(epoch);
}

Check if you need a Token Refresh

I typically put this check at the beginning of my global function for sending requests. So that when the token is expired, it automatically, and transparently gets refreshed without holding up my application.

private bool NeedRefresh(TokenResponse Token) {
return (FromEpoch(Token.not_before) < DateTime.UtcNow || FromEpoch(Token.expires_on) < DateTime.UtcNow);
}

Refresh Token

Lastly, to refresh our token, we’ll need to use that refresh_token and send a POST to https://login.microsoftonline.com/TENANTNAME/oauth2/token with the parameters:

  • client_id: Your App Client ID
  • refresh_token: Token.refresh_token
  • grant_type: “refresh_token”
  • resource: “https://api.yammer.com” or “https://graph.microsoft.com” or whichever resource URL you used in the previous steps

This will return the same type of Token we got originally, but with all new attributes so you can keep sending requests unattended.

Refreshing Token from PostMan
private async Task<TokenResponse> RefreshToken(TokenResponse token) {
string tokenUri = "https://login.microsoftonline.com/TENANTNAME/oauth2/token";
using (HttpClient client = GetClient()) {
var req = new HttpRequestMessage(HttpMethod.Post, tokenUri) { Content = new FormUrlEncodedContent(new Dictionary<string, string> {
["client_id"] = clientId,
["refresh_token"] = token.refresh_token,
["grant_type"] = "refresh_token",
["resource"] = (_type == TokenType.Graph) ? "https://graph.microsoft.com" : "https://api.yammer.com"
})
};
var res = await client.SendAsync(req);
string jsonResult = await res.Content.ReadAsStringAsync();
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(jsonResult);
if (res.StatusCode == HttpStatusCode.OK) {
if (tokenResponse.error != null) {
throw new Exception("Token refresh failed: " + tokenResponse.error);
} else {
return tokenResponse;
}
} else {
throw new Exception("Token refresh failed with HTTP Response Code " + res.StatusCode.ToString());
}
}
}

Now, you have a complete authorization flow for creating non-interactive applications that can use Delegated API’s on Azure AD. Remember to make sure that you protect your token, and client_id at all times. And never store them in plain text somewhere.

--

--