0 0
Read Time:6 Minute, 49 Second

Creating alexa skills with .NET – part 10 Alexa skill account linking.

In part 9 we setup reminders within our sample skill, we are now going to be looking at account linking.

Account linking is where we can link up an alexa user with some other form, of user account. In this example we are going to link up an alexa user with a google account but the process would be similar for any other system of identification.

Normally for account linking you would be connecting the alexa user to some existing system you have, probably one with a database, for the purposes of this demo we will create a new system to link up with and so will also need a database or somewhere to store the data and for this, this being a .NET Core series of articles we will use a .NET Core MVC deployment.

This deployment could easily be the same project as your rest services if it was not for the alexa related security that is applied to that, so we would tend to keep it as a separate deployment. We will use an Azure database and entity framework for the system, but you could use whatever you are comfortable with so long as the system is accessible from the internet.

We will add a new project to the existing Visual Studio project we have, we will call it TestyMcTestFaceAuth.

And if that works as it did for me your project structure should look like the following image (the source code will be shared later on as normal)

We are going to need a few things within this project

  • A database which we will maintain using Entity Framework, and for this project we will use code first, database first may be your requirement if you are linking to an existing system
  • A login page
  • A page to handle the callback when the user is authenticated
  • The Google OAUTH components
  • Switching on account linking for the skill in the amazon dev console

I am not going to cover all the parts of setting up EF and our new database but https://docs.microsoft.com/en-us/ef/core/get-started/aspnetcore/new-db?tabs=visual-studio would be a decent start for code first EF.

NOTE: .NET core has issues with multiple projects and EF the trick is to specify the project and be in the correct directory for the console commands, this https://stackoverflow.com/questions/52529002/setting-up-multiple-projects-with-ef-core-in-separate-project is useful.

The table for user accounts and the db context would be setup similar to the following.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;

namespace TestyMcTestFaceAuth.Models
{
    public class UserAccount
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public long ID { get; set; }
        public string UserName { get; set; }
        public string Email { get; set; }
        public string Token { get; set; }
        public string guid { get; set; }
        public string amazonclientid { get; set; }
        public string amazonresponsetype { get; set; }
        public string amazonstate { get; set; }
        public string amazonredirecturi { get; set; }

    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace TestyMcTestFaceAuth.Models
{
    public class UserDBContext : DbContext
    {
        public UserDBContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<UserAccount> UserAccounts { get; set; }
    }
}

Appsettings.json would be set as following (some values will be explained later on and I have altered sensitive data)

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "ConnectionString": {
    "TestyMCTestFaceDB": "server=xxxx.database.windows.net;database=TestyMcTestFace;User ID=TestyMcTestFace;password=xxxx;"
  },
  "AllowedHosts": "*",
  "Google": {
    "ClientID": "xxxx",
    "ClientSecret": "xxxx"
  }

}

And the startup file would have a fair amount of changes.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using TestyMcTestFaceAuth.Models;

namespace TestyMcTestFaceAuth
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(300); });


            services.AddMemoryCache();
            services.AddDbContext<UserDBContext>(opts =>
                opts.UseSqlServer(Configuration["ConnectionString:TestyMCTestFaceDB"]));
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
            services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = "Google";
                })
                .AddCookie()
                .AddOAuth("Google", options =>
                {
                    options.CallbackPath = new PathString("/signin-google");
                    options.ClientId = Configuration["Google:ClientID"];
                    options.ClientSecret = Configuration["Google:ClientSecret"];
                    options.Scope.Add("profile email"); // Google scope
                    options.AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth";
                    options.TokenEndpoint = "https://www.googleapis.com/oauth2/v4/token";
                    options.UserInformationEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo";
                    options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
                    options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
                    options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
                    options.Events = new OAuthEvents
                    {
                        OnCreatingTicket = async context =>
                        {
                            var request =
                                new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
                            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                            request.Headers.Authorization =
                                new AuthenticationHeaderValue("Bearer", context.AccessToken);
                            var response = await context.Backchannel.SendAsync(request,
                                HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
                            response.EnsureSuccessStatusCode();
                            var user = JObject.Parse(await response.Content.ReadAsStringAsync());
                            context.RunClaimActions(user);
                        }
                    };
                });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseAuthentication();
            app.UseCookiePolicy();
            app.UseSession();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller}/{action=Index}/{id?}");
            });
        }
    }
}

We have a number of setup items in the index and error pages as well, it’s probably easier to look at the source repository for these but basically we need to link to the google login page and then show the credentials if logged in.

One complication we get is that when we start to setup the google side we need to tell it where valid uses are to come from so before then we are going to publish the app at https://testymctestfaceauth.azurewebsites.net/ since it is the site URL that we will need to know.

We need to create the google project now, this involved going to https://console.developers.google.com and creating a new project

When you have this you will need to click on the item in the list to add in the callback url. If you don’t do this the signin will not be allowed

so we have the google account login now available, restricted to the domain our auth site is on and we have the OAuth credentials. (I am showing them above since once this site is working I will reset them).

We can now update the appsettings.json file in the solution and load up the site, doing so allows us to now look at switching on account linking in the alexa skill

And all being well that is basically it, the alexa user can then choose to login or not

Looking on the database we can see that a record has been created. This contains the GUID which we created as part of the linking. This is the important field.

In the JSON below we see a request coming in from the account which was linked, the GUID that we created as part of the linking process is passed to us on the request and so we could look up the users data without our own systems using that as a key.

The source code for this article is at https://github.com/raymondjstone/AlexaNetExampleStage06

Article 11, which will for the moment be the final article in this set introduces alexa notifications, this allows you to push data to an alexa not just reply to users voice commands.

About Post Author

raymond

Been developing software for 40 plus years, started with Apple ]['s moved on to a variety of mainframes, PC's, Atari ST/Falcon, Psion's then onto more web related stuff (when there was an internet) and mobile apps (Xamarin.Forms) for IOS and Android.
Happy
Happy
0 %
Sad
Sad
0 %
Excited
Excited
0 %
Sleepy
Sleepy
0 %
Angry
Angry
0 %
Surprise
Surprise
0 %

Average Rating

5 Star
0%
4 Star
0%
3 Star
0%
2 Star
0%
1 Star
0%

Leave a Reply