Azure - Key Vault

         

Scenario: Use Azure Key vault to store secrets

Solution:

Per Azure docs:

Azure Key Vault is a cloud service for securely storing and accessing secrets. A secret is anything that you want to tightly control access to, such as API keys, passwords, certificates, or cryptographic keys.
  1. Azure -> Azure Active Directory -> App registrations [left pane]->New registration
    1. Name = keyvault-myapp
    2. Who can use this application or access this API?  = Accounts in this organizational directory only (Default Directory only - Single tenant)
    3. Register
  2. On Success, Application ID (Client ID) would be displayed. Copy this for further use.
  3. Certificates & secrets -> New Client Secret 
    1. Description = client secret
    2.  Value = Copy this for further use.
  4. New resource -> Key Vault
    1. Resource group = Your resource group
    2. Key vault name = mykeyvault
    3. Region = region
    4. Review and Create
  5. Secrets [left pane]
    1. Create a secret
      1. name = keyname
      2. value = secret value
      3. Create
  6. Access policies -> Add Access Policy -> Secret permissions -> Select all -> principal -> search app registration name -> Save
  7. In ASP.NET Core -> App Settings

     "Keyvault": {
        "Vault": "keyvault-myapp",
        "ClientId": "#2",
        "ClientSecret": "#4.1"
      }

     8. Add Nuget package -> Microsoft.Extensions.Configuration.AzureKeyVault

     9. Program.cs

    namespace Web
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                CreateHostBuilder(args).Build().Run();
            }
    
            public static IHostBuilder CreateHostBuilder(string[] args) =>
               Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((context, config) =>
                {
    
                    var root = config.Build();
                    config.AddAzureKeyVault($"https://{root["KeyVault:Vault"]}.vault.azure.net/", root["KeyVault:ClientId"], root["KeyVault:ClientSecret"]);
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
        }
    }

    10. Controller

     public class MyController : Controller  
    {  
        private readonly IConfiguration _configuration;  
    
        public ValuesController(IConfiguration configuration)  
        {  
            _configuration = configuration;  
        }  
    
        [HttpGet]  
        public string Get()  
        {  
            var value = _configuration["keyname"];  
        }  
    } 

Azure - Custom DNS to App Service

        

Scenario: Map custom domain to Azure App Service

Solution:

  1. Azure -> App Services -> My Service -> Custom Domains [left pane]
  2. Copy
    1. Custom Domain Verification ID
    2. IP address
  3. Go to your domain provider, e.g. Google Domains
  4. DNS -> Add
    1. Name/Host = @
    2. Type = A
    3. Value = IP address [2.2]
  5. DNS - > Add
    1. Name/Host = asuid
    2. Type = TXT
    3. Value = Custom Domain Verification ID [2.1]
  6.  Go back to Azure -> App Service -> Custom Domains -> Add Custom Domain
  7. Fully qualified domain name per A record [#4].
  8. Validate
  9. Might take while for the new custom domain to show up in Custom Domains page.
  10. Verify navigating to the above domain.

Github Actions for continuous integration to Azure

       

Scenario: Use Github Actions for continuous integration and deploy to Azure

Solution:

Per Github:
GitHub Actions makes it easy to automate all your software workflows, now with world-class CI/CD. Build, test, and deploy your code right from GitHub. Make code reviews, branch management, and issue triaging work the way you want.

  1. Under .github folder -> create "workflows" -> Create file build-and-deploy.yml
  2.  Azure -> App Service -> Get Publish Profile -> Download file -> Copy content 
  3.  Githib -> repository -> Actions-> Secrets -> New Repository Secret [AZURE_WEBAPP_PUBLISH_PROFILE] -> Value from #3
  4. build-and-deploy.yml
  5.  name: Build and Deploy
    'on':
      - push
      - workflow_dispatch
    env:
      AZURE_WEBAPP_NAME: MyApp
      AZURE_WEBAPP_PACKAGE_PATH: ./published
      NETCORE_VERSION: '6.0'
    jobs:
      build:
        name: Build and Deploy
        runs-on: windows-latest
        steps:
          - uses: actions/checkout@v2
          - name: 'Setup .NET Core SDK ${{ env.NETCORE_VERSION }}'
            uses: actions/setup-dotnet@v1
            with:
              dotnet-version: '${{ env.NETCORE_VERSION }}'
          - name: Restore packages
            run: dotnet restore
          - name: Build app
            run: dotnet build --configuration Release --no-restore
          - name: Test app
            run: dotnet test --no-build
          - name: Publish app for deploy
            run: >-
              dotnet publish --configuration Release --no-build --output ${{
              env.AZURE_WEBAPP_PACKAGE_PATH }}
          - name: Deploy to Azure WebApp
            uses: azure/webapps-deploy@v1
            with:
              app-name: '${{ env.AZURE_WEBAPP_NAME }}'
              publish-profile: '${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}'
              package: '${{ env.AZURE_WEBAPP_PACKAGE_PATH }}'
          - name: Publish Artifacts
            uses: actions/upload-artifact@v1.0.0
            with:
              name: webapp
              path: '${{ env.AZURE_WEBAPP_PACKAGE_PATH }}'

.NET Core Middleware

      

Scenario:

MVC issue with 'The antiforgery token could not be decrypted' in web farm.

Solution:

Per MSDN:

Middleware is software that's assembled into an app pipeline to handle requests and responses. Each component:

  • Chooses whether to pass the request to the next component in the pipeline.
  • Can perform work before and after the next component in the pipeline.

Request delegates are used to build the request pipeline. The request delegates handle each HTTP request.

Request delegates are configured using Run, Map, Use extension methods.


1. HandleError

     
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Http;
    
    namespace MyCoreSolution
    {
        public class HandleError
        {
            private readonly RequestDelegate _next;
    
            public HandleError(RequestDelegate next)
            {
                _next = next;
            }
    
            public async Task Invoke(HttpContext context)
            {
                if (context.Request.Path.Value != null && (context.Request.Path.Value.Contains("Error")))
                {
                    await _next(context).ConfigureAwait(false);
    
                    switch (context.Response.StatusCode)
                    {
                        case 404:
                            HandlePageNotFound(context);
                            break;
                        default:
                            HandleException(context);
                            break;
                    }
                }
            }
    
            private static void HandleException(HttpContext context)
            {
                context.Response.Redirect("/UnknowError");
            }
    
            private static void HandlePageNotFound(HttpContext context)
            {
                context.Response.Redirect("/404");
            }
        }
    }

2. Startup.cs

     app.UseMiddleware<HandleError>();

Machine Key for ASP.NET core applications in Webfarm

     

Scenario:

MVC issue with 'The antiforgery token could not be decrypted' in web farm.

Solution:

Generate a machine key and use the same key in web.config across all instances in the web farm

1. IIS Management console
2. Go to your site -> Machine Key -> Generate Keys -> Apply
    a. Validation method = "SHA1"
b. Encryption Method = "AES"
3. The machine key would be saved to the web.config file as below

    <machineKey  
        validationKey="WDFDSFSDFSDF23423423423423WEREWRWERWEREWREW9F67897SDFDS89F7SD89F78SD97F98S7D89F7SD98F7SD98F7S9D"           
        decryptionKey="FSDGSDGSDFG6767FDSGFGFDGFDG78FD7G97FD98G"
        validation="SHA1"
        decryption="AES"
    />

Identity Server for .NET core

    

Scenario:

Setup OAuth based authentication/authorization for ASP.NET core site

Solution:

Identity Server 4 is authentication and authorization package using JSON web tokens (JWT) and it implements  OAuth 2.0 specs.

1. NugetPackage -> dotnet add package IdentityServer4
2. IValidateAppUser

    1
    2
    3
    4
        public interface IValidateAppUser
        {
            User GetUserInfo(string name, string password);
        }

3. ValidateAppUser
    

    1
    2
    3
    4
    5
     public User GetUserInfo(string name, string password)
            {
                //call app user repo
                return new User();
            }

3. IdentityConfiguration
    

    1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    public static IEnumerable<IdentityResource> IdentityResources =>
                new List<IdentityResource>
                {
                    new IdentityResources.OpenId(),
                    new IdentityResources.Profile(),
                };
    
            public static IEnumerable<ApiResource> ApiResources()
            {
                return new List<ApiResource>
                {
                    new ApiResource("ApiResourceId", "My App")
                    {
                        ApiSecrets = new List<Secret>
                        {
                            new Secret("ApiResourceKey".Sha256())
                        },
                        Scopes =
                        {
                            "offline_access",
                        },
                        UserClaims = new[] {"sub", "userId", "name", "email", "role", "returnUrl"},
                    }
                };
            }
    
            public static IEnumerable<Client> Clients()
            {
                return new[]
                {
                    new Client
                    {
                        ClientId = "ClientId",
                        ClientSecrets =
                            {new Secret("ClientSecret".Sha256())},
    
                        AllowedGrantTypes = GrantTypes.Code,
                        RedirectUris = new List<string>(),
                        RequireConsent = false,
                        RequirePkce = false,
                        PostLogoutRedirectUris = new List<string>(),
    
                        AllowedScopes = new List<string>
                        {
                            IdentityServerConstants.StandardScopes.OpenId,
                            IdentityServerConstants.StandardScopes.Profile,
                            IdentityServerConstants.StandardScopes.OfflineAccess
                        },
                        AllowOfflineAccess = true,
                        AllowAccessTokensViaBrowser = true,
    
                        AccessTokenLifetime = 5,
                        AccessTokenType = AccessTokenType.Jwt,
    
                        RefreshTokenUsage = TokenUsage.ReUse,
                        RefreshTokenExpiration = TokenExpiration.Sliding,
                    }
                };
            }
    
            public static IEnumerable<ApiScope> ApiScopes =>
                new ApiScope[]
                {
                    new ApiScope("offline_access", "My App"),
                };
        }

4.Startup.cs

    1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public void ConfigureServices(IServiceCollection services)
            {
                //Validate user
                services.AddTransient<IValidateAppUser, ValidateAppUser>();
    
                //Repo to get user info
                services.AddTransient<IAppUserRepo, AppUserRepo>();
    
    
                services.AddIdentityServer()
                    .AddSigningCredential(X509Certificate2.CreateFromEncryptedPem("", new ReadOnlySpan<char>(), "test"))
                    .AddInMemoryIdentityResources(IdentityConfiguration.IdentityResources)
                    .AddInMemoryApiScopes(IdentityConfiguration.ApiScopes)
                    .AddInMemoryApiResources(IdentityConfiguration.ApiResources())
                    .AddInMemoryClients(IdentityConfiguration.Clients());
    
                services.AddTransient<IPersistedGrantStore, GrantStore>();
    
                services.AddOptions<KeyManagementOptions>()
                    .Configure<IServiceScopeFactory>((options, factory) =>
                    {
                        options.XmlRepository = new DBKeyRepository(factory);
                    });
    
                services.AddAuthentication("OAuthCookie")
                    .AddCookie("OAuthCookie", options =>
                    {
                        options.ExpireTimeSpan =
                            new TimeSpan(10, 0, 0);
                        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
                    });
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                app.UseIdentityServer();
                app.UseAuthentication();
                app.UseMiddleware<Handler>();
                app.UseAuthorization();
    
            }


5. AccountController

      [HttpPost]
            [ValidateAntiForgeryToken]
            public async Task<IActionResult> Login(LoginInputModel model, string button)
            {
               .....
                if (ModelState.IsValid)
                {
                    var user = await _validateUser.GetUserInfo(model.Username, model.Password);
    
                        if (user != null)
                        {
                            var returnUrl = string.Empty;
    
                            if (model.ReturnUrl != null)
                            {
                                returnUrl = HttpUtility.ParseQueryString(model.ReturnUrl)["nonce"];
                            }
    
                            var claims = (new[]
                            {
                                new Claim("sub", user.UserId),
                                new Claim("user", user.UserId),
                                new Claim("name", user.Name),
                                new Claim("roles", user.Roles),
                                new Claim("returnUrl", (string.IsNullOrEmpty(returnUrl))
                            });
    
                            var userIdentity = new ClaimsIdentity(claims, "Claims");
                            var userPrincipal = new ClaimsPrincipal(new[] { userIdentity });
    
                            await HttpContext.SignInAsync(userPrincipal).ConfigureAwait(false);
                    }
                        
                }
            }
    
            [HttpPost]
            [ValidateAntiForgeryToken]
            public async Task<IActionResult> Logout(LogoutInputModel model)
            {
                // build a model so the logged out page knows what to display
                var vm = await BuildLoggedOutViewModelAsync(model.LogoutId).ConfigureAwait(false);
    
                ...
                vm.PostLogoutRedirectUri = "PostLogoutRedirectUri";
    
                if (User?.Identity.IsAuthenticated == true)
                {
                    ..
                }
    
                return View("", vm);
            }
    		
    		private async Task<LoggedOutViewModel> BuildLoggedOutViewModelAsync(string logoutId)
            {
                ..
    
                if (User?.Identity.IsAuthenticated == true)
                {
                    ..
                }
    
                return vm;
            }
    		

Identity Server - Persist Grantstore

   

Scenario:

Persist authorization grants in DB

Solution:

1. Implement custom IPersistedGrantStore

    1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using IdentityServer4.Models;
    using IdentityServer4.Stores;
    
    namespace MyCoreSolution
    {
        public class GrantStore : IPersistedGrantStore
        {
            public async Task<IEnumerable<PersistedGrant>> GetAllAsync(PersistedGrantFilter filter)
            {
    
                List<GrantInfo> grantInfos = null;
    
                await Task.Run(() =>
                {
                    //get grant by filters like subjectid, type, client etc from DB
                }).ConfigureAwait(false);
    
                return grantInfos
                    .Select(GrantInfoToGrant).ToList();
            }
    
            public async Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId)
            {
                List<GrantInfo> grantInfos = null;
    
                await Task.Run(() =>
                {
                    //get grant by subjectId from DB
                }).ConfigureAwait(false);
    
                return grantInfos
                    .Select(GrantInfoToGrant).ToList();
            }
    
            public async Task<PersistedGrant> GetAsync(string key)
            {
                List<GrantInfo> grantInfos = null;
    
                await Task.Run(() =>
                {
                    //get grant by subjectId from Key
                }).ConfigureAwait(false);
    
                return grantInfos
                    .Select(GrantInfoToGrant).FirstOrDefault();
            }
    
            public async Task RemoveAllAsync(string subjectId, string clientId, string type)
            {
                await Task.Run(() =>
                {
                    //remove grant by filters
                }).ConfigureAwait(false);
            }
    
            public async Task RemoveAsync(string key)
            {
                await Task.Run(() =>
                {
                    //remove grant by key
                }).ConfigureAwait(false);
            }
            
            public Task RemoveAllAsync(PersistedGrantFilter filter)
            {
                //remove grant by filters
    
                return Task.CompletedTask;
            }
    
            public async Task StoreAsync(PersistedGrant grant)
            {
                var grantInfo = new GrantInfo
                {
                    Key = grant.Key,
                    ClientId = grant.ClientId,
                    SubjectId = grant.SubjectId,
                    Type = grant.Type,
                    Data = grant.Data,
                    CreationTime = grant.CreationTime,
                    Expiration = grant.Expiration
                };
                //add grants
            }
    
            private PersistedGrant GrantInfoToGrant(GrantInfo grantInfo)
            {
                return new PersistedGrant()
                {
                    Key = grantInfo.Key,
                    ClientId = grantInfo.ClientId,
                    CreationTime = grantInfo.CreationTime ?? DateTime.Now,
                    Data = grantInfo.Data,
                    Expiration = grantInfo.Expiration,
                    SubjectId = grantInfo.SubjectId,
                    Type = grantInfo.Type
                };
            }
        }
    }

2.Startup.cs
    public void ConfigureServices(IServiceCollection services)
            {
                ...
                services.AddTransient<IPersistedGrantStore, GrantStore>();
            }

.NET Core - Data Protection in web farm

  

Scenario:

Persist Data Protections to DB store for web farm

Solution:

1. Implement custom IXmlRepository

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
     public class DBKeyRepository : IXmlRepository
        {
            private readonly IServiceScopeFactory _factory;
    
            public DBKeyRepository(IServiceScopeFactory factory)
            {
                _factory = factory;
            }
    
            public IReadOnlyCollection<XElement> GetAllElements()
            {
                using (var scope = _factory.CreateScope())
                {
                    return GetKeys();
                }
            }
    
            public void StoreElement(XElement element, string friendlyName)
            {
                var key = new XmlKey
                {
                    Xml = element.ToString(SaveOptions.DisableFormatting)
                };
    
                using (var scope = _factory.CreateScope())
                {
                    PersistKeys(key);
                }
            }
    
            private List<XElement> GetKeys()
            {
                var result = new List<XElement>();
    
                //get Keys to DB //XElement.Parse(xml)
    
                return result;
            }
    
            private void PersistKeys(XmlKey key)
            {
                //persist Keys to DB
            }
        }
2. XmlKey.cs

    1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    public class XmlKey
        {
            public Guid Id { get; set; }
            public string Xml { get; set; }
    
            public XmlKey()
            {
                this.Id = Guid.NewGuid();
            }
        }


3.Startup.cs
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
     public void ConfigureServices(IServiceCollection services)
            {
             ....
                services.AddOptions<KeyManagementOptions>()
                    .Configure<IServiceScopeFactory>((options, factory) =>
                    {
                        options.XmlRepository = new DBKeyRepository(factory);
                    });
             ...
            }

Move Github Sub Repository back to main repo

 -- delete .gitmodules git rm --cached MyProject/Core git commit -m 'Remove myproject_core submodule' rm -rf MyProject/Core git remo...