Skip to content

Commit

Permalink
Device flow token generation
Browse files Browse the repository at this point in the history
  • Loading branch information
Shoogn committed Apr 3, 2024
1 parent 7297772 commit 1a24b76
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 10 deletions.
2 changes: 1 addition & 1 deletion Sample/Deviceflow_ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
});

services.AddScoped<DeviceflowEndpoint>();
// services.AddHostedService<Worker>();
services.AddHostedService<Worker>();
}).Build();

var logger = host.Services.GetRequiredService<ILogger<Program>>();
Expand Down
74 changes: 70 additions & 4 deletions Sample/Deviceflow_ConsoleApp/Worker.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Deviceflow_ConsoleApp.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Text.Json;

namespace Deviceflow_ConsoleApp
{
Expand All @@ -10,26 +12,90 @@ public class Worker : BackgroundService
private readonly ILogger<Worker> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;

public Worker(ILogger<Worker> logger, IServiceScopeFactory serviceScopeFactory)
{
public Worker(ILogger<Worker> logger, IServiceScopeFactory serviceScopeFactory)
{
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while(!stoppingToken.IsCancellationRequested)
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("calling token enpoint is working normally");
// int? interval = 5000;
using var scope = _serviceScopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
var data = dbContext.DeviceFlowClients.Take(20).ToList();
if (data.Any())
{
// call token endpoint
foreach (var deviceCode in data)
await CallDeviceflowEndpointAsync(deviceCode.DeviceCode);
}
// TODO: interval should read from result.
await Task.Delay(5000, stoppingToken);
}
}

private async Task CallDeviceflowEndpointAsync(string deviceCode)
{
HttpClient client = new HttpClient();
string baseUrl = "https://localhost:7275";
Console.WriteLine("Call the token endpoint to get an access token");

var value = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("client_Id", "4"),
new KeyValuePair<string, string>("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
new KeyValuePair<string, string>("device_code", deviceCode)
};

client.BaseAddress = new Uri(baseUrl);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.ConnectionClose = true;

using var request = new HttpRequestMessage(HttpMethod.Post, "/Home/Token") { Content = new FormUrlEncodedContent(value) };
using var response = await client.SendAsync(request);

await Task.Delay(1000, stoppingToken);
if (response.IsSuccessStatusCode == false)
{
Console.WriteLine($"Calling token endpoint is faild with this status code: {response.StatusCode}");
}

using (var stream = await response.Content.ReadAsStreamAsync())
{
var deSerilaizedData = System.Text.Json.JsonSerializer.Deserialize(stream, typeof(object));

var deSerilaizedDataAsString = deSerilaizedData.ToString();
using (JsonDocument document = JsonDocument.Parse(deSerilaizedDataAsString))
{
try
{
JsonElement root = document.RootElement;
var accessTokenProperty = root.GetProperty("access_token");
var accessTokenValue = accessTokenProperty.GetString();
if(!string.IsNullOrWhiteSpace( accessTokenValue ))
{
// Now you have an access token, you can apply to whatever you want
// remove device code
using var scope = _serviceScopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
var en = dbContext.DeviceFlowClients.Find(deviceCode);
if(en != null)
{
dbContext.Remove(en);
dbContext.SaveChanges();
}
}

}
catch // ArgumentNullException / KeyNotFoundException
{
return;
}
}
}

}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using OAuth20.Server.OAuthRequest;
using OAuth20.Server.Services;
using System.Threading.Tasks;

Expand Down Expand Up @@ -27,5 +28,21 @@ public async Task<JsonResult> DeviceAuthorization()
return Json(result);
return Json("invalid client");
}

[HttpGet("~/device")]
public IActionResult Device()
{
return View();
}

[HttpPost("~/device")]
public async Task<IActionResult> Device(UserInteractionRequest userInteractionRequest)
{
var result = await _deviceAuthorizationService.DeviceFlowUserInteractionAsync(userInteractionRequest.UserCode);
if (result == true)
return RedirectToAction("Index", "Home");
else
return View(userInteractionRequest);
}
}
}
5 changes: 4 additions & 1 deletion Server/src/OAuth20.Server/Enumeration/ErrorTypeEnum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public enum ErrorTypeEnum : byte
InvalidGrant,

[Description("invalid_client")]
InvalidClient
InvalidClient,

[Description("wait_for_user_interaction")]
WaitForUserInteraction
}
}
2 changes: 1 addition & 1 deletion Server/src/OAuth20.Server/OAuth20.Server.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.10" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.16" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.24.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace OAuth20.Server.OAuthRequest
{
public class UserInteractionRequest
{
public string UserCode { get; set; }
}
}
1 change: 1 addition & 0 deletions Server/src/OAuth20.Server/OauthRequest/TokenRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ public class TokenRequest
public string redirect_uri { get; set; }
public string code_verifier { get; set; }
public IList<string> scope { get; set; }
public string device_code { get; set; }
}
}
50 changes: 50 additions & 0 deletions Server/src/OAuth20.Server/Services/AuthorizeResultService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ where scopes.Contains(m)

public TokenResponse GenerateToken(TokenRequest tokenRequest)
{
// TODO: this method needs a refactor, and need to call generate token validation method

var result = new TokenResponse();
var serchBySecret = _clientService.SearchForClientBySecret(tokenRequest.grant_type);

Expand All @@ -159,6 +161,54 @@ public TokenResponse GenerateToken(TokenRequest tokenRequest)
return new TokenResponse { Error = checkClientResult.Error, ErrorDescription = checkClientResult.ErrorDescription };
}

// Check first if the authorization_grant is DeviceCode...
// then generate the jwt access token and store it to back store
if (tokenRequest.grant_type == AuthorizationGrantTypesEnum.DeviceCode.GetEnumDescription())
{
var clientHasDeviceCodeGrant = checkClientResult.Client.GrantTypes.Contains(tokenRequest.grant_type);
if (!clientHasDeviceCodeGrant)
{
result.Error = ErrorTypeEnum.InvalidGrant.GetEnumDescription();
return result;
}

var deviceCode = _context.DeviceFlows.Where(x => x.DeviceCode == tokenRequest.device_code &&
x.ClientId == tokenRequest.client_id).SingleOrDefault();

if (deviceCode == null)
{
result.Error = ErrorTypeEnum.InvalidRequest.GetEnumDescription();
result.ErrorDescription = "Please check the device code";
return result;
}

if (deviceCode.UserInterActionComplete == false)
{
result.Error = ErrorTypeEnum.WaitForUserInteraction.GetEnumDescription();
return result;
}


if (deviceCode.ExpireIn < DateTime.Now)
{
result.Error = ErrorTypeEnum.InvalidRequest.GetEnumDescription();
result.ErrorDescription = "The device code is expired";
return result;
}

var requestedScope = deviceCode.RequestedScope.Split(' ');
IEnumerable<string> scopes = checkClientResult.Client.AllowedScopes.Intersect(requestedScope);


var deviceflowAccessTokenResult = generateJWTTokne(scopes, Constants.TokenTypes.JWTAcceseccToken, checkClientResult.Client, null);
SaveJWTTokenInBackStore(checkClientResult.Client.ClientId, deviceflowAccessTokenResult.AccessToken, deviceflowAccessTokenResult.ExpirationDate);

result.access_token = deviceflowAccessTokenResult.AccessToken;
result.id_token = null;
return result;
}


// Check first if the authorization_grant is client_credentials...
// then generate the jwt access token and store it to back store

Expand Down
34 changes: 31 additions & 3 deletions Server/src/OAuth20.Server/Services/DeviceAuthorizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Everyone is permitted to copy and distribute verbatim copies
using Microsoft.Extensions.Options;
using OAuth20.Server.Models.Context;
using OAuth20.Server.Models.Entities;
using System.Diagnostics.Contracts;

namespace OAuth20.Server.Services;

Expand All @@ -37,6 +38,33 @@ BaseDBContext dBContext
_dbContext = dBContext;
}

public async Task<bool> DeviceFlowUserInteractionAsync(string userCode)
{
if (string.IsNullOrWhiteSpace(userCode))
return false;

var data = await _dbContext.DeviceFlows.FindAsync(userCode);
if (data != null)
{
if (data.ExpireIn > DateTime.Now)
{
data.UserInterActionComplete = true;
_dbContext.Update(data);
var result = await _dbContext.SaveChangesAsync();
if (result > 0)
{
return true;
}
return false;
}
return false;
}
else
{
return false;
}
}

public async Task<DeviceAuthorizationResponse> GenerateDeviceAuthorizationCodeAsync(HttpContext httpContext)
{
var validationResult = await _validation.ValidateAsync(httpContext);
Expand All @@ -51,9 +79,9 @@ public async Task<DeviceAuthorizationResponse> GenerateDeviceAuthorizationCodeAs
UserCode = GenerateUserCode(),
DeviceCode = GenerateDeviceCode(),
VerificationUri = _options.IDPUri + "/device",
ExpiresIn = 60,
ExpiresIn = 300, // user code and device code are valid for 5 minutes.
Interval = _options.DeviceFlowInterval,

};

// Store the responst in the back store (sql server in my case)
Expand All @@ -67,7 +95,7 @@ public async Task<DeviceAuthorizationResponse> GenerateDeviceAuthorizationCodeAs
UserInterActionComplete = false,
SessionId = httpContext.Session.Id,
RequestedScope = validationResult.RequestedScope != null ? validationResult.RequestedScope : default,

};

_dbContext.Add(deviceflowEntity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ namespace OAuth20.Server.Services
public interface IDeviceAuthorizationService
{
Task<DeviceAuthorizationResponse> GenerateDeviceAuthorizationCodeAsync(HttpContext httpContext);
Task<bool> DeviceFlowUserInteractionAsync(string userCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@model OAuth20.Server.OAuthRequest.UserInteractionRequest
@{
ViewData["Title"] = "Device";
Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Device</h1>

<div class="container" style="margin-top:30px;margin-bottom:130px">
<div class="row justify-content-md-center" style="margin-top:130px;">
<div class="col-md-5">
<div class="card">
<h5 class="card-header bg-dark text-white">Device</h5>
<div class="card-body">

<form class="px-4 py-3" asp-action="Device" asp-controller="DeviceAuthorizationEndpoint" method="post">
<div class="form-group">
<label for="exampleDropdownFormEmail1">User Code</label>
<input type="text" asp-for="UserCode" class="form-control" id="exampleDropdownFormEmail1" placeholder="enter the user code here">
</div>

<button type="submit" class="btn btn-primary">Accept</button>
</form>
</div>
</div>
</div>


</div>




</div>

0 comments on commit 1a24b76

Please sign in to comment.