Added OAuth Flow
This commit is contained in:
Родитель
a40d649f38
Коммит
8ace522b1a
|
@ -7,7 +7,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamarinAzureChallenge.Andro
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamarinAzureChallenge.iOS", "XamarinAzureChallenge\XamarinAzureChallenge.iOS\XamarinAzureChallenge.iOS.csproj", "{2C81F630-FC66-4BEC-A800-56A0C743F3D3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XamarinAzureChallenge", "XamarinAzureChallenge\XamarinAzureChallenge\XamarinAzureChallenge.csproj", "{906E148B-01D5-4106-9C5B-404A8B655069}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamarinAzureChallenge", "XamarinAzureChallenge\XamarinAzureChallenge\XamarinAzureChallenge.csproj", "{906E148B-01D5-4106-9C5B-404A8B655069}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XamarinAzureChallenge.Functions", "XamarinAzureChallenge\XamarinAzureChallenge.Functions\XamarinAzureChallenge.Functions.csproj", "{2A44CE99-0A65-4AE7-897F-D2EA679A42B7}"
|
||||
EndProject
|
||||
|
|
31
src/XamarinAzureChallenge/XamarinAzureChallenge.Android/MainActivity.cs
Executable file → Normal file
31
src/XamarinAzureChallenge/XamarinAzureChallenge.Android/MainActivity.cs
Executable file → Normal file
|
@ -1,11 +1,15 @@
|
|||
using Android.App;
|
||||
using System;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using XamarinAzureChallenge.Pages;
|
||||
|
||||
namespace XamarinAzureChallenge.Droid
|
||||
{
|
||||
[Activity(Label = "XamarinAzureChallenge", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
|
||||
[Activity(Label = "XamarinAzureChallenge", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ScreenOrientation = ScreenOrientation.Portrait)]
|
||||
[IntentFilter(new string[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable }, DataSchemes = new[] { "xamarinazurechallenge" })]
|
||||
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
|
||||
{
|
||||
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Permission[] grantResults)
|
||||
|
@ -25,6 +29,29 @@ namespace XamarinAzureChallenge.Droid
|
|||
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
|
||||
|
||||
LoadApplication(new App());
|
||||
|
||||
if (Intent?.Data is Android.Net.Uri callbackUri)
|
||||
ExecuteCallbackUri(callbackUri);
|
||||
}
|
||||
|
||||
async void ExecuteCallbackUri(Android.Net.Uri callbackUri)
|
||||
{
|
||||
if (Xamarin.Forms.Application.Current.MainPage is Xamarin.Forms.NavigationPage navigationPage)
|
||||
{
|
||||
navigationPage.Pushed += HandlePushed;
|
||||
|
||||
await navigationPage.PushAsync(new UserDataPage());
|
||||
|
||||
async void HandlePushed(object sender, Xamarin.Forms.NavigationEventArgs e)
|
||||
{
|
||||
if (e.Page is UserDataPage)
|
||||
{
|
||||
navigationPage.Pushed -= HandlePushed;
|
||||
|
||||
await AzureAuthenticationService.AuthorizeSession(new Uri(callbackUri.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.XamarinAzureChallenge">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.xamarin.XamarinAzureChallenge">
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<application android:label="XamarinAzureChallenge.Android"></application>
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
|
||||
<TargetFrameworkVersion>v9.0</TargetFrameworkVersion>
|
||||
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
|
||||
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
</PropertyGroup>
|
||||
|
@ -33,7 +34,7 @@
|
|||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<DebugType>none</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release</OutputPath>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace Microsoft.XamarinAzureChallenge.AZF
|
|||
private static HttpClient Client => clientHolder.Value;
|
||||
|
||||
[FunctionName(nameof(SubmitChallengeFunction))]
|
||||
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "post")][FromBody] User user, ILogger log, ExecutionContext context)
|
||||
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = nameof(SubmitChallengeFunction) + "/{azureSubscriptionId}")][FromBody] User user, ILogger log, ExecutionContext context, string azureSubscriptionId)
|
||||
{
|
||||
log.LogInformation("HTTP Triggered");
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
namespace XamarinAzureChallenge.Shared
|
||||
{
|
||||
public class AzureAuthenticationCompletedEventArgs : EventArgs
|
||||
{
|
||||
public AzureAuthenticationCompletedEventArgs(bool isAuthenticationSuccessful) =>
|
||||
IsAuthenticationSuccessful = isAuthenticationSuccessful;
|
||||
|
||||
public bool IsAuthenticationSuccessful { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace XamarinAzureChallenge.ViewModels
|
||||
{
|
||||
public class AzureClientIdModel
|
||||
{
|
||||
public string ClientId { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace XamarinAzureChallenge.Shared
|
||||
{
|
||||
public class AzureToken
|
||||
{
|
||||
[JsonProperty("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
[JsonProperty("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
|
||||
[JsonProperty("expires_in")]
|
||||
public long ExpiresIn { get; set; }
|
||||
|
||||
[JsonProperty("expires_on")]
|
||||
public long ExpiresOn { get; set; }
|
||||
|
||||
[JsonProperty("resource")]
|
||||
public Uri Resource { get; set; }
|
||||
|
||||
[JsonProperty("refresh_token")]
|
||||
public string RefreshToken { get; set; }
|
||||
|
||||
[JsonProperty("scope")]
|
||||
public string Scope { get; set; }
|
||||
|
||||
[JsonProperty("id_token")]
|
||||
public string IdToken { get; set; }
|
||||
}
|
||||
}
|
3
src/XamarinAzureChallenge/XamarinAzureChallenge.Shared/XamarinAzureChallenge.Shared.projitems
Executable file → Normal file
3
src/XamarinAzureChallenge/XamarinAzureChallenge.Shared/XamarinAzureChallenge.Shared.projitems
Executable file → Normal file
|
@ -10,6 +10,9 @@
|
|||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Models\User.cs" Condition=" '$(EnableDefaultCompileItems)' == 'true' " />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Models\AzureToken.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Models\AzureClientIdModel.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Models\AzureAuthenticationCompletedEventArgs.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Models\" />
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
using Foundation;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Foundation;
|
||||
using SafariServices;
|
||||
using UIKit;
|
||||
using Xamarin.Forms;
|
||||
using XamarinAzureChallenge.ViewModels;
|
||||
|
||||
namespace XamarinAzureChallenge.iOS
|
||||
{
|
||||
|
@ -13,5 +18,53 @@ namespace XamarinAzureChallenge.iOS
|
|||
|
||||
return base.FinishedLaunching(uiApplication, launchOptions);
|
||||
}
|
||||
|
||||
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
|
||||
{
|
||||
var callbackUri = new Uri(url.AbsoluteString);
|
||||
|
||||
HandleCallbackUri(callbackUri);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async void HandleCallbackUri(Uri callbackUri)
|
||||
{
|
||||
await CloseSFSafariViewController();
|
||||
await AzureAuthenticationService.AuthorizeSession(callbackUri);
|
||||
}
|
||||
|
||||
async Task CloseSFSafariViewController()
|
||||
{
|
||||
while (await GetVisibleViewController() is SFSafariViewController sfSafariViewController)
|
||||
{
|
||||
await Device.InvokeOnMainThreadAsync(async () =>
|
||||
{
|
||||
await sfSafariViewController.DismissViewControllerAsync(true);
|
||||
sfSafariViewController.Dispose();
|
||||
sfSafariViewController = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Task<UIViewController> GetVisibleViewController()
|
||||
{
|
||||
return Device.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
var rootController = UIApplication.SharedApplication.KeyWindow.RootViewController;
|
||||
|
||||
switch (rootController.PresentedViewController)
|
||||
{
|
||||
case UINavigationController navigationController:
|
||||
return navigationController.TopViewController;
|
||||
case UITabBarController tabBarController:
|
||||
return tabBarController.SelectedViewController;
|
||||
case null:
|
||||
return rootController;
|
||||
default:
|
||||
return rootController.PresentedViewController;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,41 +2,50 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>1</integer>
|
||||
<integer>2</integer>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>8.0</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>XamarinAzureChallenge</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.companyname.XamarinAzureChallenge</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>XamarinAzureChallenge</string>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>roboto.regular.ttf</string>
|
||||
</array>
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>1</integer>
|
||||
<integer>2</integer>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>XamarinAzureChallenge</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.xamarin.XamarinAzureChallenge</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>XamarinAzureChallenge</string>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>roboto.regular.ttf</string>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.xamarin.XamarinAzureChallenge</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>xamarinazurechallenge</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -13,12 +13,13 @@
|
|||
<IPhoneResourcePrefix>Resources</IPhoneResourcePrefix>
|
||||
<AssemblyName>XamarinAzureChallenge.iOS</AssemblyName>
|
||||
<MtouchHttpClientHandler>NSUrlSessionHandler</MtouchHttpClientHandler>
|
||||
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhoneSimulator' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<DebugType>portable</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\iPhoneSimulator\Debug</OutputPath>
|
||||
<DefineConstants>DEBUG</DefineConstants>
|
||||
|
@ -43,7 +44,7 @@
|
|||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhone' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<DebugType>portable</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\iPhone\Debug</OutputPath>
|
||||
<DefineConstants>DEBUG</DefineConstants>
|
||||
|
|
2
src/XamarinAzureChallenge/XamarinAzureChallenge/Pages/ResultPage.xaml.cs
Executable file → Normal file
2
src/XamarinAzureChallenge/XamarinAzureChallenge/Pages/ResultPage.xaml.cs
Executable file → Normal file
|
@ -4,7 +4,7 @@ using XamarinAzureChallenge.ViewModels;
|
|||
namespace XamarinAzureChallenge.Pages
|
||||
{
|
||||
public partial class ResultPage : BaseContentPage<ResultViewModel>
|
||||
{
|
||||
{
|
||||
public ResultPage(HttpStatusCode statusCode)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
<Setter Property="FontSize"
|
||||
Value="12" />
|
||||
<Setter Property="TextColor"
|
||||
Value="Gray" />
|
||||
Value="Black" />
|
||||
<Setter Property="Margin"
|
||||
Value="4,0,0,0" />
|
||||
</Style>
|
||||
|
@ -64,6 +64,8 @@
|
|||
Value="Black" />
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="White" />
|
||||
<Setter Property="PlaceholderColor"
|
||||
Value="Gray" />
|
||||
</Style>
|
||||
<Style x:Key="HyperLinkStyle"
|
||||
TargetType="Label">
|
||||
|
@ -121,6 +123,7 @@
|
|||
Grid.ColumnSpan="2"
|
||||
Placeholder="Enter your name"
|
||||
Style="{StaticResource EntryStyle}"
|
||||
ReturnType="Next"
|
||||
Text="{Binding User.Name}" />
|
||||
|
||||
<Label Grid.Row="4"
|
||||
|
@ -131,6 +134,7 @@
|
|||
Grid.ColumnSpan="2"
|
||||
Keyboard="Email"
|
||||
Placeholder="Enter your email"
|
||||
ReturnType="Next"
|
||||
Style="{StaticResource EntryStyle}"
|
||||
Text="{Binding User.Email}" />
|
||||
|
||||
|
@ -183,7 +187,8 @@
|
|||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
|
||||
<Button Grid.Row="14"
|
||||
<Button x:Name="SubmitButton"
|
||||
Grid.Row="14"
|
||||
Grid.ColumnSpan="2"
|
||||
Padding="10"
|
||||
Command="{Binding SubmitCommand}"
|
||||
|
|
20
src/XamarinAzureChallenge/XamarinAzureChallenge/Pages/UserDataPage.xaml.cs
Executable file → Normal file
20
src/XamarinAzureChallenge/XamarinAzureChallenge/Pages/UserDataPage.xaml.cs
Executable file → Normal file
|
@ -3,11 +3,19 @@
|
|||
namespace XamarinAzureChallenge.Pages
|
||||
{
|
||||
public partial class UserDataPage : BaseContentPage<UserDataViewModel>
|
||||
{
|
||||
public UserDataPage ()
|
||||
{
|
||||
InitializeComponent ();
|
||||
{
|
||||
public UserDataPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
}
|
||||
}
|
||||
BindingContext = new UserDataViewModel();
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
ViewModel.IsBusy = false;
|
||||
|
||||
base.OnAppearing();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Newtonsoft.Json;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
using XamarinAzureChallenge.Shared;
|
||||
using XamarinAzureChallenge.ViewModels;
|
||||
|
||||
namespace XamarinAzureChallenge
|
||||
{
|
||||
public static class AzureAuthenticationService
|
||||
{
|
||||
const string _oauthTokenKey = "OAuthToken";
|
||||
|
||||
private static readonly Lazy<HttpClient> clientHolder = new Lazy<HttpClient>();
|
||||
private static readonly Lazy<JsonSerializer> serializer = new Lazy<JsonSerializer>();
|
||||
|
||||
private static string _sessionAuthenticationId;
|
||||
|
||||
public static event EventHandler AuthorizeSessionStarted;
|
||||
public static event EventHandler<AzureAuthenticationCompletedEventArgs> AuthenticationCompleted;
|
||||
|
||||
private static HttpClient Client => clientHolder.Value;
|
||||
private static JsonSerializer Serializer => serializer.Value;
|
||||
|
||||
public static async Task AuthorizeSession(Uri callbackUri)
|
||||
{
|
||||
OnAuthorizeSessionStarted();
|
||||
|
||||
var code = HttpUtility.ParseQueryString(callbackUri.Query).Get("code");
|
||||
var state = HttpUtility.ParseQueryString(callbackUri.Query).Get("state");
|
||||
var errorDescription = HttpUtility.ParseQueryString(callbackUri.Query).Get("error_description");
|
||||
|
||||
if (string.IsNullOrEmpty(code))
|
||||
errorDescription = "Invalid Authorization Code";
|
||||
|
||||
if (state != _sessionAuthenticationId)
|
||||
errorDescription = "Invalid SessionId";
|
||||
|
||||
if (string.IsNullOrEmpty(errorDescription))
|
||||
{
|
||||
_sessionAuthenticationId = string.Empty;
|
||||
|
||||
var clientId = await GetAzureClientId();
|
||||
|
||||
var content = new Dictionary<string,string>
|
||||
{
|
||||
{ "grant_type", "authorization_code" },
|
||||
{ "client_id", clientId },
|
||||
{ "code", code },
|
||||
{ "redirect_uri", $"{nameof(XamarinAzureChallenge).ToLower()}://auth" }
|
||||
};
|
||||
|
||||
using (var response = await Client.PostAsync("https://login.microsoftonline.com/common/oauth2/token", new FormUrlEncodedContent(content)))
|
||||
{
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var azureToken = JsonConvert.DeserializeObject<AzureToken>(responseContent);
|
||||
|
||||
await SaveAzureToken(azureToken);
|
||||
|
||||
OnAuthenticationCompleted(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Application.Current.MainPage.DisplayAlert("Azure Authentication Unsuccessful", responseContent, "Ok");
|
||||
OnAuthenticationCompleted(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await Application.Current.MainPage.DisplayAlert("Azure Authentication Unsuccessful", errorDescription, "Ok");
|
||||
OnAuthenticationCompleted(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<AzureToken> GetAzureToken()
|
||||
{
|
||||
var serializedToken = await SecureStorage.GetAsync(_oauthTokenKey).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var token = JsonConvert.DeserializeObject<AzureToken>(serializedToken);
|
||||
|
||||
if (token is null)
|
||||
return new AzureToken();
|
||||
|
||||
return token;
|
||||
}
|
||||
catch (ArgumentNullException)
|
||||
{
|
||||
return new AzureToken();
|
||||
}
|
||||
catch (JsonReaderException)
|
||||
{
|
||||
return new AzureToken();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task OpenAuthenticationPage()
|
||||
{
|
||||
var azureClientId = await GetAzureClientId();
|
||||
|
||||
_sessionAuthenticationId = Guid.NewGuid().ToString();
|
||||
|
||||
var azureLoginUrl = $"https://login.microsoftonline.com/common/oauth2/authorize?client_id={azureClientId}&response_type=code&state={_sessionAuthenticationId}";
|
||||
|
||||
await Device.InvokeOnMainThreadAsync(() => Browser.OpenAsync(azureLoginUrl));
|
||||
}
|
||||
|
||||
static async Task<string> GetAzureClientId()
|
||||
{
|
||||
using (var stream = await Client.GetStreamAsync("https://xamarinazurechallenge-private.azurewebsites.net/api/GetClientId"))
|
||||
using (var streamReader = new StreamReader(stream))
|
||||
using (var json = new JsonTextReader(streamReader))
|
||||
{
|
||||
var azureClientIdModel = Serializer.Deserialize<AzureClientIdModel>(json);
|
||||
return azureClientIdModel.ClientId;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SaveAzureToken(AzureToken token)
|
||||
{
|
||||
if (token is null)
|
||||
throw new ArgumentNullException(nameof(token));
|
||||
|
||||
if (token.AccessToken is null)
|
||||
throw new ArgumentNullException(nameof(token.AccessToken));
|
||||
|
||||
var serializedToken = JsonConvert.SerializeObject(token);
|
||||
await SecureStorage.SetAsync(_oauthTokenKey, serializedToken);
|
||||
}
|
||||
|
||||
private static void OnAuthorizeSessionStarted() => AuthorizeSessionStarted?.Invoke(null, EventArgs.Empty);
|
||||
|
||||
private static void OnAuthenticationCompleted(bool isAuthenticationSuccessful) =>
|
||||
AuthenticationCompleted?.Invoke(null, new AzureAuthenticationCompletedEventArgs(isAuthenticationSuccessful));
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -10,13 +11,15 @@ namespace XamarinAzureChallenge.ViewModels
|
|||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected void SetAndRaisePropertyChanged<TRef>(ref TRef field, TRef value, [CallerMemberName] string propertyName = "")
|
||||
protected void SetAndRaisePropertyChanged<T>(ref T field, T value, Action onChanged = null, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (EqualityComparer<TRef>.Default.Equals(field, value))
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
return;
|
||||
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
|
||||
onChanged?.Invoke();
|
||||
}
|
||||
|
||||
protected Task NavigateToPage(Page page) => Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushAsync(page));
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Newtonsoft.Json;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
using XamarinAzureChallenge.Pages;
|
||||
using XamarinAzureChallenge.Shared;
|
||||
using XamarinAzureChallenge.Shared.Models;
|
||||
|
||||
namespace XamarinAzureChallenge.ViewModels
|
||||
{
|
||||
public class UserDataViewModel : BaseViewModel
|
||||
{
|
||||
#error Missing Azure Function Endpoint Url. Replace "Enter Your Function API Url Here" with your Azure Function Endopint Url
|
||||
//#error Missing Azure Function Endpoint Url. Replace "Enter Your Function API Url Here" with your Azure Function Endopint Url
|
||||
private const string endpoint = "Enter Your Function API Url Here";
|
||||
private readonly Lazy<HttpClient> clientHolder = new Lazy<HttpClient>();
|
||||
|
||||
|
@ -21,18 +23,21 @@ namespace XamarinAzureChallenge.ViewModels
|
|||
|
||||
public UserDataViewModel()
|
||||
{
|
||||
User = new User();
|
||||
SubmitCommand = new Command(async () => await SubmitCommmandExecute(User));
|
||||
User = GetSavedUser();
|
||||
|
||||
SubmitCommand = new Command(async () => await SubmitCommmandExecute(User), () => !IsBusy);
|
||||
PrivacyStatementCommand = new Command(async () => await PrivacyStatementCommandExecute());
|
||||
|
||||
AzureAuthenticationService.AuthorizeSessionStarted += HandleAuthorizeSessionStarted;
|
||||
}
|
||||
|
||||
public ICommand SubmitCommand { get; }
|
||||
public ICommand PrivacyStatementCommand { get; }
|
||||
public Command SubmitCommand { get; }
|
||||
public Command PrivacyStatementCommand { get; }
|
||||
|
||||
public bool IsBusy
|
||||
{
|
||||
get => isBusy;
|
||||
set => SetAndRaisePropertyChanged(ref isBusy, value);
|
||||
set => SetAndRaisePropertyChanged(ref isBusy, value, () => Device.BeginInvokeOnMainThread(SubmitCommand.ChangeCanExecute));
|
||||
}
|
||||
|
||||
public User User
|
||||
|
@ -45,62 +50,154 @@ namespace XamarinAzureChallenge.ViewModels
|
|||
|
||||
private async Task SubmitCommmandExecute(User submittedUser)
|
||||
{
|
||||
if (IsBusy)
|
||||
return;
|
||||
var areFieldsValid = await AreFieldsValid(submittedUser.Name, submittedUser.Email, submittedUser.Phone, submittedUser.IsTermsOfServiceAccepted);
|
||||
|
||||
IsBusy = true;
|
||||
|
||||
try
|
||||
if (areFieldsValid)
|
||||
{
|
||||
var areFieldsValid = await AreFieldsValid(submittedUser.Name, submittedUser.Email, submittedUser.Phone, submittedUser.IsTermsOfServiceAccepted);
|
||||
IsBusy = true;
|
||||
|
||||
if (areFieldsValid)
|
||||
{
|
||||
var serializedUser = JsonConvert.SerializeObject(User);
|
||||
SaveUser(User);
|
||||
|
||||
var content = new StringContent(serializedUser, Encoding.UTF8, "application/json");
|
||||
|
||||
var result = await Client.PostAsync(endpoint, content);
|
||||
|
||||
await NavigateToPage(new ResultPage(result.StatusCode));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
await NavigateToPage(new ResultPage(default));
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
await AzureAuthenticationService.OpenAuthenticationPage();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> AreFieldsValid(string name, string email, string phone, bool isTermsOfServiceAccepted)
|
||||
private void HandleAuthorizeSessionStarted(object sender, EventArgs e)
|
||||
{
|
||||
var result = false;
|
||||
//Ensure the SubmitButton remains disabled until Authorization has completed
|
||||
IsBusy = true;
|
||||
|
||||
// Listen for AuthenticationCompleted event
|
||||
AzureAuthenticationService.AuthenticationCompleted += HandleAuthenticationCompleted;
|
||||
|
||||
// Always unsubscribe events to avoid memory leaks
|
||||
AzureAuthenticationService.AuthorizeSessionStarted -= HandleAuthorizeSessionStarted;
|
||||
}
|
||||
|
||||
private async void HandleAuthenticationCompleted(object sender, AzureAuthenticationCompletedEventArgs e)
|
||||
{
|
||||
// Always unsubscribe events to avoid memory leaks
|
||||
AzureAuthenticationService.AuthenticationCompleted -= HandleAuthenticationCompleted;
|
||||
|
||||
if (e.IsAuthenticationSuccessful)
|
||||
{
|
||||
var azureToken = await AzureAuthenticationService.GetAzureToken();
|
||||
var subscriptionId = await GetAzureSubscriptionId(azureToken);
|
||||
|
||||
await SubmitUserInfo(User, subscriptionId);
|
||||
}
|
||||
|
||||
IsBusy = false;
|
||||
}
|
||||
|
||||
|
||||
private async Task<string> GetAzureSubscriptionId(AzureToken azureToken)
|
||||
{
|
||||
Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(azureToken.TokenType, azureToken.AccessToken);
|
||||
|
||||
using (var response = await Client.GetAsync("https://management.azure.com/subscriptions?api-version=2016-06-01"))
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
return responseContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
await Application.Current.MainPage.DisplayAlert("Get Azure Subscription Id Failed", "", "Ok");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubmitUserInfo(User submittedUser, string azureSubscriptionId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var serializedUser = JsonConvert.SerializeObject(submittedUser);
|
||||
|
||||
using (var content = new StringContent(serializedUser, Encoding.UTF8, "application/json"))
|
||||
using (var response = await Client.PostAsync($"{endpoint}/{azureSubscriptionId}", content))
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
await NavigateToPage(new ResultPage(response.StatusCode));
|
||||
}
|
||||
else
|
||||
{
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
await Application.Current.MainPage.DisplayAlert("Submission Unsuccessful", responseContent, "Ok");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Application.Current.MainPage.DisplayAlert("Submission Unsuccessful", ex.Message, "Ok");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> AreFieldsValid(string name, string email, string phone, bool isTermsOfServiceAccepted, bool shouldDisplayAlert = true)
|
||||
{
|
||||
var areFieldsValid = false;
|
||||
var errorMessage = "";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
await DisplayInvalidFieldAlert("Name cannot be blank");
|
||||
errorMessage = "Name cannot be blank";
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
await DisplayInvalidFieldAlert("Email cannot be blank");
|
||||
errorMessage = "Email cannot be blank";
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(phone))
|
||||
{
|
||||
await DisplayInvalidFieldAlert("Phone cannot be blank");
|
||||
errorMessage = "Phone cannot be blank";
|
||||
}
|
||||
else if (!isTermsOfServiceAccepted)
|
||||
{
|
||||
await DisplayInvalidFieldAlert("Terms of Service Not Accepted");
|
||||
errorMessage = "Terms of Service Not Accepted";
|
||||
}
|
||||
else
|
||||
{
|
||||
result = true;
|
||||
areFieldsValid = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
if (!areFieldsValid && shouldDisplayAlert)
|
||||
await DisplayInvalidFieldAlert(errorMessage);
|
||||
|
||||
return areFieldsValid;
|
||||
}
|
||||
|
||||
private User GetSavedUser()
|
||||
{
|
||||
var serializedUser = Preferences.Get(nameof(User), string.Empty);
|
||||
|
||||
try
|
||||
{
|
||||
var token = JsonConvert.DeserializeObject<User>(serializedUser);
|
||||
|
||||
if (token is null)
|
||||
return new User();
|
||||
|
||||
return token;
|
||||
}
|
||||
catch (ArgumentNullException)
|
||||
{
|
||||
return new User();
|
||||
}
|
||||
catch (JsonReaderException)
|
||||
{
|
||||
return new User();
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveUser(User currentUser)
|
||||
{
|
||||
if (currentUser is null)
|
||||
throw new ArgumentNullException(nameof(currentUser));
|
||||
|
||||
var serializedUser = JsonConvert.SerializeObject(currentUser);
|
||||
Preferences.Set(nameof(User), serializedUser);
|
||||
}
|
||||
|
||||
private Task PrivacyStatementCommandExecute() =>
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<DebugType>portable</DebugType>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
@ -16,6 +17,7 @@
|
|||
<ItemGroup>
|
||||
<Folder Include="Pages\" />
|
||||
<Folder Include="ViewModels\" />
|
||||
<Folder Include="Services\" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\XamarinAzureChallenge.Shared\XamarinAzureChallenge.Shared.projitems" Label="Shared" Condition="Exists('..\XamarinAzureChallenge.Shared\XamarinAzureChallenge.Shared.projitems')" />
|
||||
</Project>
|
Загрузка…
Ссылка в новой задаче