Notes on Adding .NET Core 2.2 Identity to Existing Project

Even though all my projects are in .NET Core now, I rarely get the opportunity to use Identity because of my work with our backend Legacy system.  Recently, though, I built a very lightweight SEO Management system for one of our sites (that allows a 3rd party to tweak our page titles, meta tags, etc) and wanted to give them user access and roles.

The entire project can be found on GitHub, but below is just a running list of sites I used to get this done, noting all the troubleshooting and stupid little mistakes I did along the way.

Adding Identity to an Already Existing Project

This part was relatively easy and the Microsoft documents provided an easy enough guide.  I believe I had some issues:

CS1902 C# Invalid option for /debug; must be full or pdbonly – with the Data Migrations because I didn’t have EntityFramework installed.   Basically, all errors in this phase were not as presented – they were mostly because I was lacking packages to migrate.

To ensure I had all the right packages installed, i used: Microsoft.AspNetCore.App -Version 2.2.6

Also, in this area, I decided not to put the connection string in appsettings.json, opting instead to use System Environment Variables both in development and in the future on Azure.  I changed my IdentityHostingStartup.cs file to this:

public void Configure(IWebHostBuilder builder)
{
    builder.ConfigureServices((context, services) =>
    {
        services.AddDbContext<DbContext>(options =>
        options.UseSqlServer(System.Environment.GetEnvironmentVariable("SQLAZURECONNSTR_Production")));

Configuring Email

I wanted to test everything first, with no email confirmation.  So, using Microsoft Docs I added this email class.  The only change is again, I prefer System Environment Variables, so my EmailSender class differs:

public class EmailSender : IEmailSender
{
    private string apiKey = System.Environment.GetEnvironmentVariable("SENDGRID_APIKEY");

    public Task SendEmailAsync(string email, string subject, string message)
    {
        return Execute(subject, message, email);
    }

    public Task Execute(string subject, string message, string email)
    {
        var client = new SendGridClient(apiKey);
        var msg = new SendGridMessage()
        {
            From = new EmailAddress("webmaster@mycompany.com", "SEO Management"),
            Subject = subject,
            PlainTextContent = message,
            HtmlContent = message
        };
        msg.AddTo(new EmailAddress(email));

        // Disable click tracking.
        // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
        msg.SetClickTracking(false, false);

        return client.SendEmailAsync(msg);
    }
}

Once I finished with the Microsoft Docs email setup, I started projected, registered 1 user (under email I want to be super admin).

Customizing User Roles & Seeding Admin

Now, I needed to assign that user the role of super admin and also create other user roles.  I used the following answer to get started added both methods to Startup.cs in the Configure method.

https://stackoverflow.com/questions/55994082/how-to-create-roles-in-asp-net-core-2-2-and-assign-them-to-users?answertab=votes#tab-top

            app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
 
 
    // this is just to seed other admin role, run once
    //CreateAdminRole(serviceProvider).Wait();

}
 
// this is just to seed other admin role, run once
private async Task CreateAdminRole(IServiceProvider serviceProvider)
{
    var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
    var UserManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
 
    IdentityResult roleResult;
 
    var roleCheck = await RoleManager.RoleExistsAsync("Admin");
    if (!roleCheck)
    {
        roleResult = await RoleManager.CreateAsync(new IdentityRole("Admin"));
    }
 
    ApplicationUser user = await UserManager.FindByEmailAsync("myemail@mycompany.com");
    await UserManager.AddToRoleAsync(user, "Admin");
}
 

After following that guide, the one error I kept getting here was:  No service for type ‘Microsoft.AspNetCore.Identity.RoleManager’.   

and another:  Unable to resolve service for type ‘Microsoft.AspNetCore.Identity.IRoleStore`1[Microsoft.AspNetCore.Identity.IdentityRole]’ while attempting to activate ‘Microsoft.AspNetCore.Identity.RoleManager`1[Microsoft.AspNetCore.Identity.IdentityRole]’.

The first was because I simply forgot to .AddRoles to the configuration of the DefaultIdentity.  The second error is that ORDER MATTERS.  AddRoles goes before AddEntityFramework.

Both resolved with this:

services.AddDefaultIdentity<ApplicationUser>()
            .AddRoles<IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>();

Finishing with the guide, I discovered another page that needed changing: _ManageNav which also had a SignIn @inject (like _LoginPartial) that needed to be changed:

@inject SignInManager<ApplicationUser> SignInManager

And then: InvalidOperationException: Unable to resolve service for type ‘Microsoft.AspNetCore.Identity.UserManager`1[

Which, again, is all pointing to the use of IdentityUser instead of your new ApplicationUser.  You’ll need to go through each of your Razor pages and change IdentityUser to ApplicationUser.

Which will lead to you scaffolding the views (if you hadn’t already) …

Scaffolding UI for Access to Razor Pages & Views

Now that the set up was working, I wanted a better look at my pages and views.  By default, in Core 2.1 the UI comes in a prebuilt package, but you can easily scaffold it to view and change as you like: ASP.NET Core 2.2 – Scaffold Identity UI.  I even did this over (ie, a 2nd time) over my first steps and it recognized all the previous code and just scaffolded nicely, no harm done.

Adding User Roles

At this point, any user can register, but they can not log in unless they confirm their email.  Once they DO confirm their email, they can log in but have no access to any of the pages because they have yet to be assigned a role.  The administrator must do this before they can continue.

Using the same method that I used to create the seed admin, I added a method to startup to seed the user roles:

// this is just to seed user roles, run once
private async Task CreateUserRoles(IServiceProvider serviceProvider)
{
    var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();

    IdentityResult roleResult;

    var roleCheck = await RoleManager.RoleExistsAsync("SEOAdmin");
    if (!roleCheck)
    {
        roleResult = await RoleManager.CreateAsync(new IdentityRole("SEOAdmin"));
    }

    roleCheck = await RoleManager.RoleExistsAsync("SEOManager");
    if (!roleCheck)
    {
        roleResult = await RoleManager.CreateAsync(new IdentityRole("SEOManager"));
    }

}

Then, added

CreateUserRoles(serviceProvider).Wait();

To Configure method in Startup.cs, ran once and the roles were set.

Assigning Roles to Users

Then, I wanted something I could see existing users and decide their role before they got access.  The end result looks like this:

To do this, I added a ManageUsersController and retrieved Users and Roles so that I could assign them a role.  I created a the User Role View Model and User View Model to reflect the drop downs and added a corresponding view.

That was the last step to getting this little SEO backend together.

All packaged together, the entire project can all be seen on Github.

 

Continue Reading

First Heroku App: Small API in PHP to Talk to .Net Core

I’ve recently been curious about switching to a time API for my time stamps and removing any dependency the app might have on the server for a timestamp.  Upon Googling I found some paid services, some free and of the free ones, I noticed one was hosted on Heroku.  I’ve heard of Heroku, but never had a reason to attempt to use it.  This was the perfect chance.

How I Created a Small “GetTime” API

First, I created a free account on Heroku, nothing special.  After verifying my email, I logged in to my Heroku Dashboard and up on the right hand corner, selected Create New App.  I named it my company-api and out popped an app.

I decided on just plain, legacy PHP and a simple DateTime string passed thru JSON encode, just to get started.  No authentication, no timezone,  just a simple spit out if a request to the site came, like this:

<?php
    header('Content-type:application/json;charset=utf-8');

    $DateTime = new DateTime();
    $currentTime = $DateTime->format("Y-m-d H:i:s");
    echo json_encode($currentTime);
?>

I created a Git repo for this brand new file and pushed it out.  Then, I went back to Heroku, Dashboard, My App and Deploy.  I selected Github as my deploy “resource” and selected the new repo I just made along with correpsonding branch.

I hit manual deploy and Heroku runs off to my GitHub repo, grabs the code, compiles and publishes.

It failed.

Beginner Troubles

My first problem was that Heroku could not determine what language I had chosen for my app (you’d think the <?php would give it away …).  You need either one of two things:  a composer.json file or an index.php file (for legacy, like mine).  I renamed my file to index.php and all I needed now was a “builder pack”.

To add a builder pack, I went back to Heroku, Dashboard, My App and Settings.  Under builder pack, I added one for: “php”.  Save settings and done.

I went back to Deploy, Manual Deploy and had a successful output.  Yay!  First Heroku app!

Adding Some Security

I want to make sure this API is receiving and sending JSON, so there’s a few IF’s I make the request go through before I hit logic on my PHP page.  I also want to (lightly) secure the requests made to this API and monitor our usage of it for metrics information (and future investment).  Since this itty bitty API is just relying on 1 index.php file, I figure this can be a sort of “router” for future API’s.  So, this is what I added to the final PHP file:

  • Verify the request is in JSON
  • Require two variables passed:
    • a secret api “key”
    • an api “request”
  • Use a switch statement to forward to needed method

 

The final, simple 1 page PHP Heroku API:

<?php
header('Content-type:application/json;charset=utf-8');

//Make sure that it is a POST request.
if(strcasecmp($_SERVER['REQUEST_METHOD'], 'POST') != 0){
    throw new Exception('Request method must be POST!');
}

//Make sure that the content type of the POST request has been set to application/json
$contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';
$contentIsJson = strpos($contentType, "application/json");

if ($contentIsJson === false){
    throw new Exception('Content type must be: application/json');
}

//Receive the RAW post data.
$content = trim(file_get_contents("php://input"));

//Attempt to decode the incoming RAW post data from JSON.
$decoded = json_decode($content, true);

$app = strtoupper($decoded['API']);
$key = $decoded['APIKEY'];

//verify user key - simple MD5 generator: http://onlinemd5.com/.  will build user management for keys if ever needed
if ( $key == "BEAF1CB722A3F7758C7A7FA43F6BF2D1" )
{   

    switch ($app) {
        case "TIME":
            $jsonString = getTime();
            $arr = array('datetime' => $jsonString);
            break;
        default:
            $arr = array('error' => "Unknown Request On API");
            break;
    }  

    echo json_encode($arr);
}


//return the current time
function getTime ()
{
    $DateTime = new DateTime();

    //by default heroku returns time in UTC - can change in dashboard, config vars, only use as needed below
    //$DateTime->modify('-6 hours');
    $currentTime = $DateTime->format("Y-m-d H:i:s");

    $jsonString = $currentTime;

    return $jsonString;
}

?>

 

Verifying the Response

I used Postman to send a raw JSON request to my Heroku app (used their default/free url).  I wanted to make sure all my problems were resolved with this new toy, first, and then move on.  Here’s what the raw request and response look like on Postman:

Heroku + my PHP are responding nicely!

Handling the Request and Response in C#

So here’s how I did the same request and received the response in C# (I use dotnet Core):

public async Task<string> OurHerokuAPI()
{
    string reqUrl = "https://mycompany-api.herokuapp.com";

    using (var client = new HttpClient())
    {
        client.DefaultRequestHeaders
            .Accept
            .Add(new MediaTypeWithQualityHeaderValue("application/json"));

        try
        {

            var query = new
            {
                APIKEY = "BEAF1CB722A3F7758C7A7FA43F6BF2D1",
                API = "time"
            };

            var asJson = JsonConvert.SerializeObject(query);
            HttpResponseMessage response = await client.PostAsync(reqUrl, new StringContent(asJson, Encoding.UTF8, "application/json"));

            if (response.IsSuccessStatusCode)
            {
                var definition = new { datetime = string.Empty };
                var json = JsonConvert.DeserializeAnonymousType(response.Content.ReadAsStringAsync().Result, definition);

                time = json.datetime;

                //monitors success across various "time" api's. in case this particular one fails, there can be various backups until success flag returns true.
                success = true;
            }
        }
        catch (OperationCanceledException)
        {
            //TODO: CREATE ERROR MESSAGE SEND BACK TO EMAIL/ERROR 
        }
    }
    return time;
}

 

If you see notes, yes, I decided to have some fun and create maybe two other functions, just like the one above, where I query some “free” time api’s until that “success” flag turns true.  After x tries, I reluctantly call my last function, that gets the server timestamp.  As said, I wanted my timestamp to be independent of server.  So, if it hits this last method, I also send myself an error email that the time API is failing.

In the future, I could  use environment variables (config vars) more wisely, instead of hardcoding.  There’s also so much clean up to do, but this was a very fun intro into Heroku!

Continue Reading

Connecting to and Calling An AS400 Program with i5_toolkit

I’m starting to document some of the ways I’ve begun to use Aura’s EasyCom i5 Connect package to connect PHP to our as400. This has been a long process of trying to webify some of our programs and get the information out.  (btw, we’re a small business and yearly the package is about $600 – very, very reasonable.)

Currently, we have a webserver (Windows Server) that hosts our website. On it, we installed the required Aura software (a Windows exe GUI and a PHP extension) to connect to our as400. The PHP pages then utilize that PHP extension to make calls to our as400. On our as400 side, our programmer created small programs to access the larger programs we need. So, I pass say 3 parameters via PHP, make a call via Ericom i5, and then the RPG does the brainwork. It passes back my requested parameters and sets up a datafile I can query using basic ODBC and mySQL. We may change this in the future, but we like the logic work taking place in RPG on the as400 (where all our data resides anyway) and PHP being the fetcher of such data.

So, first up,  here’s how I connect to the as400 (after initial setup):

	/******************************************************
	*  AS400 Management
	*******************************************************/
		// connect to the iSeries / prepare for procedure call - port can be set up thru Aura GUI
		// keeping username and password in session to be able to connect to as400 as needed
		function i5_conn_AS400(){
			$conn = i5_connect('as400.mydomain.com:9999', $_SESSION['CONN_USER'], $_SESSION['CONN_PASSWORD']);

			if (!$conn) {
				$error = i5_error();
				echo " Error during connection\n";
				echo "<BR> Error number: ".$error["num"];
				echo "<BR> Error category: ".$error["cat"];
				echo "<BR> Error message: ".$error["msg"];
				echo "<BR> Error description: ".$error["desc"];
				trigger_error("I5 connection fails", E_USER_ERROR);
				exit();
			}
			return $conn;
		}

Then, here’s the preparation of parameters and call to the as400:

<?php
/* Set the Value of the Parameters */
$desc = array (
array ("name"=>"PARM1", "io"=>I5_INOUT, "type"=>I5_TYPE_CHAR, "length"=> 50),
array ("name"=>"PARM2", "io"=>I5_INOUT, "type"=> I5_TYPE_PACKED, "length"=> "8.0"),
array ("name"=>"PARM3", "io"=>I5_INOUT, "type"=>I5_TYPE_CHAR, "length"=> 1)
);

$prog = i5_program_prepare("PGMLIB/WEBLIB", $desc);

if ($prog === FALSE) {
$errorTab = i5_error();
echo "Program prepare failed <br>\n";
var_dump($errorTab);
die();
}

/* Execute Program */
$params = array ("PARM1"=>$sessID,"PARM2"=>$PARM2,"PARM3"=>$PARM3);
$retvals = array ("PARM1"=>"PARM1","PARM2"=>"PARM2","PARM3"=>"PARM3");

$ret = i5_program_call($prog, $params, $retvals) ;

if (!$ret){
print_r(i5_error());
trigger_error("i5_execute error : ".i5_errormsg(), E_USER_ERROR);
}

$ret = i5_program_close ($prog);

if (!$ret){
trigger_error("i5_program_close error : ".i5_errormsg(), E_USER_ERROR);
}
?>

I’ve actually created a connection “class” for both the Aura i5 connect and the odbc connect. That way when needed, I can simply call a function and connect to each as needed. I’m still VERY new to this all (and I just learned PHP), but figure I’d document some of this process as I go along. I have two sites currently running on this logic.

Continue Reading