Начал изучать asp.net core и первое что пытался найти это некое подобие «OAuthAuthorizationServerProvider» для реализации генерации тикета и «IAuthenticationTokenProvider» для реализаций рефреш токена как в обычном asp.net, но не нашел. Не исключено, что плохо искал, и может появится коммент типа «вот обкатанная библиотека для этого дела».

Хорошо, что можно довольно просто написать свой Middleware для обработки запросов, и связать его с некоторой реализацией провайдера через «сервисы» в ConfigureServices.

Итак, мне нужно что бы были методы для получения токена по логину и паролю по адресу "/token" и метод по получению токена по рефреш-токену.

   public interface IOAuthProvider
    {
        Task ByPassword(OAuthProviderContext oAuthProviderContext);
        Task ByRefreshToken(OAuthProviderContext oAuthProviderContext);
    }

То есть в конечном итоге реализовав один этот интерфейс и связав его в ConfigureServices авторизация должна работать.

В параметре «OAuthProviderContext» будут храниться данные контекста для авторизации:

    public class OAuthProviderContext
    {
        public bool HasError { get; private set; }
        public string Error { get; private set; }
        public string AccessToken { get; private set; }
        public string RefreshToken { get; set; }

        public string ClientId { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
        public void SetError(string error)
        {
            Error = error;
            HasError = true;
        }
        public void SetToken(string access_token, string refresh_token)
        {
            AccessToken = access_token;
            RefreshToken = refresh_token;
        }
    }

Теперь надо сделать middleware, которое будет работать с будущими реализациями IOAuthProvider:

class OAuthProviderMiddleware
    {
        RequestDelegate _next;
        IOAuthProvider _oAuthProvider;       

        public OAuthProviderMiddleware(RequestDelegate next, IOAuthProvider oAuthProvider)
        {
            _next = next;
            _oAuthProvider = oAuthProvider;
        }
        public async Task Invoke(HttpContext context)
        {
            OAuthProviderContext _oAuthProviderContext;

            string path = context.Request.Path.Value.ToLower().Trim();

            bool isPost = context.Request.Method.ToLower() == "post";

            if (path == "/token" && isPost)
            {
                var form = context.Request.Form;

                if (!form.ContainsKey("grant_type")) {
                    await context.BadRequest("invalid grant_type");
                    return;
                }
                if (!form.ContainsKey("client_id"))
                {
                    await context.BadRequest("invalid client_id");
                    return;
                }

                string grant_type = form["grant_type"];
                string client_id = form["client_id"];

                switch (grant_type)
                {
                    case "password":
                        {
                            if (!form.ContainsKey("username"))
                            {
                                await context.BadRequest("invalid username");
                                return;
                            }
                            if (!form.ContainsKey("password"))
                            {
                                await context.BadRequest("invalid password");
                                return;
                            }

                            string username = form["username"];
                            string password = form["password"];

                            _oAuthProviderContext = new OAuthProviderContext()
                            {
                                ClientId = client_id,
                                Username = username,
                                Password = password
                            };


                            await _oAuthProvider.ByPassword(_oAuthProviderContext);

                            if (_oAuthProviderContext.HasError)
                            {
                                await context.BadRequest(_oAuthProviderContext.Error);
                                return;
                            }
                            else
                            {
                                await context.WriteToken(_oAuthProviderContext);
                                return;
                            }

                        };
                    case "refresh_token":
                        {
                            if (!form.ContainsKey("refresh_token"))
                            {
                                await context.BadRequest("invalid refresh_token");
                                return;
                            }

                            string refresh_token = form["refresh_token"];

                            _oAuthProviderContext = new OAuthProviderContext()
                            {
                                ClientId = client_id,
                                RefreshToken = refresh_token
                            };

                            await _oAuthProvider.ByRefreshToken(_oAuthProviderContext);

                            if (_oAuthProviderContext.HasError)
                            {
                                await context.BadRequest(_oAuthProviderContext.Error);                                
                                return;
                            }
                            else
                            {
                                await context.WriteToken(_oAuthProviderContext);
                                return;
                            }
                        };
                    default:
                        {
                            await context.BadRequest("invalid grant_type");
                            return;
                        };
                }
            }
            else
            {
                await _next.Invoke(context);
            }
        }
    }

    public static class OAuthExtensions
    {
        public static IApplicationBuilder UseOAuth(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<OAuthProviderMiddleware>();
        }

        internal static async Task BadRequest(this HttpContext context, string Error)
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsync(Error);
        }

        internal static async Task WriteToken(this HttpContext context, OAuthProviderContext _oAuthProviderContext)
        {
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsync(JsonConvert.SerializeObject(new
            {
                access_token = _oAuthProviderContext.AccessToken,
                refresh_token = _oAuthProviderContext.RefreshToken
            }));
        }
    }

В Configure потом можно будет вызвать обертку

app.UseOAuth();

Далее остается написать конкретную реализацию IOAuthProvider.

Токен будет в виде JWT, а рефреш-токен рандомный byte[] массив длиной 100 представленный как Base64. В конце еще будет ссылка на код.

 public class OAuthProviderImplement : IOAuthProvider
    {
        IServiceProvider _services;
        IOptions<AuthOptions> _authOptions = null;
        Helper _helper = null;

        public OAuthProviderImplement(IServiceProvider services, IOptions<AuthOptions> authOptions, Helper helper)
        {
            _services = services;
            _authOptions = authOptions;
            _helper = helper;
        }

        public async Task ByPassword(OAuthProviderContext context)
        {
            ClaimsIdentity identity = await GetIdentity(context.Username, context.ClientId, context.Password);
            if (identity == null)
            {
                context.SetError("User not found");
                return;
            }

            string encodedJwt = CreateJWT(identity);
            string refresh_token = await CreateRefreshToken(context.ClientId, identity);

            if (refresh_token == null)
            {
                context.SetError("Error while create refresh token");
                return;
            }

            context.SetToken(encodedJwt, refresh_token);
            return;
        }

        public async Task ByRefreshToken(OAuthProviderContext context)
        {
            ProtectedTicket protectedTicket = await GrantRefreshToken(context.RefreshToken);

            if (protectedTicket == null)
            {
                context.SetError("Invalid refresh token");
                return;
            }

            if (protectedTicket.clientid != context.ClientId)
            {
                context.SetError("Invalid client id");
                return;
            }

            ClaimsIdentity identity = await GetIdentity(protectedTicket.username, protectedTicket.clientid);

            if (identity == null)
            {
                context.SetError("User not found");
                return;
            }

            string encodedJwt = CreateJWT(identity);

            context.SetToken(encodedJwt, context.RefreshToken);
            return;
        }

        string CreateJWT(ClaimsIdentity identity)
        {
            var now = DateTime.UtcNow;
            // создаем JWT-токен
            var jwt = new JwtSecurityToken(
                    issuer: _authOptions.Value.Issuer,
                    audience: _authOptions.Value.Audience,
                    notBefore: now,
                    claims: identity.Claims,
                    expires: now.Add(TimeSpan.FromSeconds(_authOptions.Value.LifetimeSeconds)),
                    signingCredentials: new SigningCredentials(_authOptions.Value.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256));

            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

            return encodedJwt;
        }

        async Task<ClaimsIdentity> GetIdentity(string username, string clientid, string password = null)
        {
            using (var serviceScope = _services.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                IAuthRepository _repo = serviceScope.ServiceProvider.GetService<IAuthRepository>();

                ApplicationUser user = null;
                if (password != null)
                {
                    user = await _repo.FindUser(username, password);
                }
                else
                {
                    user = await _repo.FindUser(username);
                }

                if (user != null)
                {
                    var claims = new List<Claim>
                {
                    new Claim(ClaimsIdentity.DefaultNameClaimType, user.UserName)
                };

                    foreach (var r in await _repo.GetRoles(user))
                    {
                        claims.Add(new Claim(ClaimsIdentity.DefaultRoleClaimType, r));
                    }

                    claims.Add(new Claim("client_id", clientid));

                    ClaimsIdentity claimsIdentity = new ClaimsIdentity(
                        claims,
                        "Password",
                        ClaimsIdentity.DefaultNameClaimType,
                        ClaimsIdentity.DefaultRoleClaimType);

                    return claimsIdentity;
                }
                else
                {

                }

                // если пользователя не найдено
                return null;
            }
        }

        async Task<string> CreateRefreshToken(string clientid, ClaimsIdentity claimsIdentity)
        {
            using (var serviceScope = _services.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                IAuthRepository _repo = serviceScope.ServiceProvider.GetService<IAuthRepository>();
                Client client = _repo.FindClient(clientid);

                var refreshTokenId = _helper.GetHash(_helper.GenerateRandomCryptographicKey(100));

                var refreshTokenLifeTime = client.RefreshTokenLifeTime;

                var now = DateTime.UtcNow;

                var token = new RefreshToken()
                {
                    Id = _helper.GetHash(refreshTokenId),
                    ClientId = clientid,
                    Subject = claimsIdentity.Name,
                    IssuedUtc = now,
                    ExpiresUtc = now.AddMinutes(Convert.ToDouble(refreshTokenLifeTime))
                };

                token.ProtectedTicket = JsonConvert.SerializeObject(new ProtectedTicket { clientid = clientid, username = claimsIdentity.Name });

                var result = await _repo.AddRefreshToken(token);

                if (result)
                {
                    return refreshTokenId;
                }

                return null;
            }
        }

        async Task<ProtectedTicket> GrantRefreshToken(string refreshTokenId)
        {
            using (var serviceScope = _services.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                IAuthRepository _repo = serviceScope.ServiceProvider.GetService<IAuthRepository>();
                string hashedTokenId = _helper.GetHash(refreshTokenId);
                ProtectedTicket protectedTicket = null;

                var refreshToken = await _repo.FindRefreshToken(hashedTokenId);

                if (refreshToken != null)
                {
                    //Get protectedTicket from refreshToken class
                    protectedTicket = JsonConvert.DeserializeObject<ProtectedTicket>(refreshToken.ProtectedTicket);

                    return protectedTicket;
                }
                else
                {
                    return null;
                }
            }
        }
    }

Теперь обработаются запросы:
post "/token" с данными в body: grant_type="password", client_id="ngAuth", username="admin", password="123"
и
post "/token" с данными в body: grant_type="refresh_token", client_id="ngAuth", refresh_token="dgDrVQHylvHmi8QZ5oThVjWyqdrLYKhp1/XHsIJI65g="

На этом все, вообщем напишите кто че думает.

Весь код

Комментарии (14)


  1. x893
    22.09.2018 21:21
    +1

    Сделали — хорошо, но это рутинный процесс, что SAML, что OAuth, что остальные. Полдня работы не повод писать заметку. Проще на github и посмотреть звёзды, последователей или орков.


    1. Ernado
      23.09.2018 13:01
      +1

      На минутку завис, пытаясь понять, когда орки успели добраться до гитхаба, и что они там делают :)


      1. x893
        24.09.2018 11:36

        :) ф выпала :)


  1. ioppoi
    22.09.2018 23:16

    спасибо


  1. lair
    23.09.2018 00:59

    Не исключено, что плохо искал, и может появится коммент типа «вот обкатанная библиотека для этого дела».

    Ну так IdentityServer же.


    1. dmitry_dvm
      23.09.2018 09:54

      Причем в самих офдоках написано про несколько провайдеров, включая ids4.


    1. Ascar Автор
      23.09.2018 16:51

      Возможно я переборщил с названием, вообщем переименовал, так как мне не надо ouath 2.0, а по сути просто вход/токен/рефреш-токен. Про IdentityServer я знаю.


      1. lair
        23.09.2018 19:36

        Не надо изобретать собственную авторизацию на основе токена, когда есть OAuth 2, поэтому все равно берите IdentityServer. Там нужная вам функциональность есть, и она реализована с минимальным количеством ошибок.


        1. gerod
          23.09.2018 20:51

          а есть пример кода, где токен сохраняется, либо генерируется 2-й для одного и того же пользователя, но авторизованного с разных устройств?


          1. lair
            23.09.2018 21:15

            Опишите сценарий подробнее, я не понимаю, чего вы хотите добиться.


            1. gerod
              24.09.2018 15:15

              меня интересует пример использования OAuth 2, без использования собственных велосипедов в сценарии:
              1 пользователь может авторизоваться с разных приложений(десктоп, плагин в браузере). Одновременная работа под одним акаунтом с разных приложений.
              Я писал свой собственный метод авторизации и он мне не очень нравится, но пока работает — отлично.
              В гайдах asp.net oauth 2 показана авторизация только с 1-го приложения. Если попытаться получить для другого приложения токен, то токен у первого становиться недействительным. Если есть где то гайд — буду благодарен за ссылку.
              И да — я новичок в данной области поэтому просьба отнестись с пониманием.


              1. lair
                24.09.2018 15:40
                +1

                Если попытаться получить для другого приложения токен, то токен у первого становиться недействительным.

                Это очень странно. У разных приложений должен быть разный client_id; токены для разных client_id независимы. Более того, спецификация OAuth 2 не требует инвалидации токена и в рамках одного client_id (логично же: одно приложение может быть установлено на нескольких компьютерах); по умолчанию нормальные реализации просто выдают новый токен, не трогая старый.


                Возьмите IdentityServer, он прикручивается в минимальное количество движений, в нем работает именно так.


        1. Ascar Автор
          23.09.2018 21:06

          Не думаю, что целесообразно делать это для единичного приложения. Если бы было несколько приложений в которых нужна такая сквозная авторизация то да. Вообщем изначально цель другая.


          1. lair
            23.09.2018 21:17

            Вообщем изначально цель другая.

            Какая же? У вас в коде показан обычный такой Resource Owner Password Flow, им и пользуйтесь. Изобретать велосипеды в безопасности плохо.