From 1a24b7643290e01859eefc5e48fe4c2addb3b4d8 Mon Sep 17 00:00:00 2001 From: Mohammed Ahmed Hussien Date: Thu, 4 Apr 2024 00:25:23 +0300 Subject: [PATCH] Device flow token generation --- Sample/Deviceflow_ConsoleApp/Program.cs | 2 +- Sample/Deviceflow_ConsoleApp/Worker.cs | 74 ++++++++++++++++++- .../DeviceAuthorizationEndpointController.cs | 17 +++++ .../Enumeration/ErrorTypeEnum.cs | 5 +- .../src/OAuth20.Server/OAuth20.Server.csproj | 2 +- .../OAuthRequest/UserInteractionRequest.cs | 7 ++ .../OauthRequest/TokenRequest.cs | 1 + .../Services/AuthorizeResultService.cs | 50 +++++++++++++ .../Services/DeviceAuthorizationService.cs | 34 ++++++++- .../Interfaces/IDeviceAuthorizationService.cs | 1 + .../DeviceAuthorizationEndpoint/Device.cshtml | 35 +++++++++ 11 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 Server/src/OAuth20.Server/OAuthRequest/UserInteractionRequest.cs create mode 100644 Server/src/OAuth20.Server/Views/DeviceAuthorizationEndpoint/Device.cshtml diff --git a/Sample/Deviceflow_ConsoleApp/Program.cs b/Sample/Deviceflow_ConsoleApp/Program.cs index 3d1460f..ef3318f 100644 --- a/Sample/Deviceflow_ConsoleApp/Program.cs +++ b/Sample/Deviceflow_ConsoleApp/Program.cs @@ -24,7 +24,7 @@ }); services.AddScoped(); - // services.AddHostedService(); + services.AddHostedService(); }).Build(); var logger = host.Services.GetRequiredService>(); diff --git a/Sample/Deviceflow_ConsoleApp/Worker.cs b/Sample/Deviceflow_ConsoleApp/Worker.cs index cdc19f7..117e14d 100644 --- a/Sample/Deviceflow_ConsoleApp/Worker.cs +++ b/Sample/Deviceflow_ConsoleApp/Worker.cs @@ -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 { @@ -10,26 +12,90 @@ public class Worker : BackgroundService private readonly ILogger _logger; private readonly IServiceScopeFactory _serviceScopeFactory; - public Worker(ILogger logger, IServiceScopeFactory serviceScopeFactory) - { + public Worker(ILogger 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(); 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> + { + new KeyValuePair("client_Id", "4"), + new KeyValuePair("grant_type", "urn:ietf:params:oauth:grant-type:device_code"), + new KeyValuePair("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(); + var en = dbContext.DeviceFlowClients.Find(deviceCode); + if(en != null) + { + dbContext.Remove(en); + dbContext.SaveChanges(); + } + } + + } + catch // ArgumentNullException / KeyNotFoundException + { + return; + } + } + } + } } } diff --git a/Server/src/OAuth20.Server/Controllers/DeviceAuthorizationEndpointController.cs b/Server/src/OAuth20.Server/Controllers/DeviceAuthorizationEndpointController.cs index 68f0f69..9462803 100644 --- a/Server/src/OAuth20.Server/Controllers/DeviceAuthorizationEndpointController.cs +++ b/Server/src/OAuth20.Server/Controllers/DeviceAuthorizationEndpointController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using OAuth20.Server.OAuthRequest; using OAuth20.Server.Services; using System.Threading.Tasks; @@ -27,5 +28,21 @@ public async Task DeviceAuthorization() return Json(result); return Json("invalid client"); } + + [HttpGet("~/device")] + public IActionResult Device() + { + return View(); + } + + [HttpPost("~/device")] + public async Task Device(UserInteractionRequest userInteractionRequest) + { + var result = await _deviceAuthorizationService.DeviceFlowUserInteractionAsync(userInteractionRequest.UserCode); + if (result == true) + return RedirectToAction("Index", "Home"); + else + return View(userInteractionRequest); + } } } diff --git a/Server/src/OAuth20.Server/Enumeration/ErrorTypeEnum.cs b/Server/src/OAuth20.Server/Enumeration/ErrorTypeEnum.cs index 0828c00..09928d0 100644 --- a/Server/src/OAuth20.Server/Enumeration/ErrorTypeEnum.cs +++ b/Server/src/OAuth20.Server/Enumeration/ErrorTypeEnum.cs @@ -36,6 +36,9 @@ public enum ErrorTypeEnum : byte InvalidGrant, [Description("invalid_client")] - InvalidClient + InvalidClient, + + [Description("wait_for_user_interaction")] + WaitForUserInteraction } } diff --git a/Server/src/OAuth20.Server/OAuth20.Server.csproj b/Server/src/OAuth20.Server/OAuth20.Server.csproj index 4addcc6..daf3d6f 100644 --- a/Server/src/OAuth20.Server/OAuth20.Server.csproj +++ b/Server/src/OAuth20.Server/OAuth20.Server.csproj @@ -18,7 +18,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Server/src/OAuth20.Server/OAuthRequest/UserInteractionRequest.cs b/Server/src/OAuth20.Server/OAuthRequest/UserInteractionRequest.cs new file mode 100644 index 0000000..b55e340 --- /dev/null +++ b/Server/src/OAuth20.Server/OAuthRequest/UserInteractionRequest.cs @@ -0,0 +1,7 @@ +namespace OAuth20.Server.OAuthRequest +{ + public class UserInteractionRequest + { + public string UserCode { get; set; } + } +} diff --git a/Server/src/OAuth20.Server/OauthRequest/TokenRequest.cs b/Server/src/OAuth20.Server/OauthRequest/TokenRequest.cs index 1c994fc..05cd664 100644 --- a/Server/src/OAuth20.Server/OauthRequest/TokenRequest.cs +++ b/Server/src/OAuth20.Server/OauthRequest/TokenRequest.cs @@ -19,5 +19,6 @@ public class TokenRequest public string redirect_uri { get; set; } public string code_verifier { get; set; } public IList scope { get; set; } + public string device_code { get; set; } } } diff --git a/Server/src/OAuth20.Server/Services/AuthorizeResultService.cs b/Server/src/OAuth20.Server/Services/AuthorizeResultService.cs index 32f63f4..1fd8803 100644 --- a/Server/src/OAuth20.Server/Services/AuthorizeResultService.cs +++ b/Server/src/OAuth20.Server/Services/AuthorizeResultService.cs @@ -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); @@ -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 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 diff --git a/Server/src/OAuth20.Server/Services/DeviceAuthorizationService.cs b/Server/src/OAuth20.Server/Services/DeviceAuthorizationService.cs index 3d5758f..9d8cf65 100644 --- a/Server/src/OAuth20.Server/Services/DeviceAuthorizationService.cs +++ b/Server/src/OAuth20.Server/Services/DeviceAuthorizationService.cs @@ -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; @@ -37,6 +38,33 @@ BaseDBContext dBContext _dbContext = dBContext; } + public async Task 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 GenerateDeviceAuthorizationCodeAsync(HttpContext httpContext) { var validationResult = await _validation.ValidateAsync(httpContext); @@ -51,9 +79,9 @@ public async Task 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) @@ -67,7 +95,7 @@ public async Task GenerateDeviceAuthorizationCodeAs UserInterActionComplete = false, SessionId = httpContext.Session.Id, RequestedScope = validationResult.RequestedScope != null ? validationResult.RequestedScope : default, - + }; _dbContext.Add(deviceflowEntity); diff --git a/Server/src/OAuth20.Server/Services/Interfaces/IDeviceAuthorizationService.cs b/Server/src/OAuth20.Server/Services/Interfaces/IDeviceAuthorizationService.cs index a5e71f3..f277d93 100644 --- a/Server/src/OAuth20.Server/Services/Interfaces/IDeviceAuthorizationService.cs +++ b/Server/src/OAuth20.Server/Services/Interfaces/IDeviceAuthorizationService.cs @@ -7,5 +7,6 @@ namespace OAuth20.Server.Services public interface IDeviceAuthorizationService { Task GenerateDeviceAuthorizationCodeAsync(HttpContext httpContext); + Task DeviceFlowUserInteractionAsync(string userCode); } } diff --git a/Server/src/OAuth20.Server/Views/DeviceAuthorizationEndpoint/Device.cshtml b/Server/src/OAuth20.Server/Views/DeviceAuthorizationEndpoint/Device.cshtml new file mode 100644 index 0000000..a5161d6 --- /dev/null +++ b/Server/src/OAuth20.Server/Views/DeviceAuthorizationEndpoint/Device.cshtml @@ -0,0 +1,35 @@ +@model OAuth20.Server.OAuthRequest.UserInteractionRequest +@{ + ViewData["Title"] = "Device"; + Layout = "~/Views/Shared/_Layout.cshtml"; +} + +

Device

+ +
+
+
+
+
Device
+
+ +
+
+ + +
+ + +
+
+
+
+ + +
+ + + + +
+