web-dev-qa-db-ja.com

Webapi2.0アクセストークンの有効期限が切れたときに更新JWTトークンを実装する方法

私はWebAPI実装にまったく慣れていないので、ASP.net web formアプリケーションとHttpClientオブジェクトを使用するいくつかのスタンドアロンアプリケーション(C#コンソール/ Windowsアプリケーション)で使用するWebAPIサービスを作成しました。

Web APIに有効期限付きの基本的なJWTアクセストークン認証を実装しました。この認証手法は、トークンの有効期限が切れるまでトークンが期限切れになるまで正常に機能します。これは認証の実装では問題ありませんが、トークンが更新/参照できるようにWeb APIに更新トークンロジックを実装し、クライアントがWebAPIリソースを使用できるようにします。

私はたくさんググったが、リフレッシュトークンロジックの適切な実装を見つけることができなかった。期限切れのアクセストークンを処理するための正しいアプローチがある場合は、私を助けてください。

以下は、asp.netアプリケーションでWebAPIを使用するために実行した手順です。

  1. ASP.net Webフォームのログインページで、Web APIを「TokenController」と呼びました。このコントローラーは、loginIDとpasswordの2つの引数を取り、セッションオブジェクトに保存したJWTトークンを返します。

  2. これで、クライアントアプリケーションでWeb APIリソースを使用する必要がある場合は常に、httpclientを使用してWebAPIを呼び出すときに、リクエストヘッダーでアクセストークンを送信する必要があります。

  3. しかし、トークンの有効期限が切れると、クライアントはWeb APIリソースを使用できなくなり、再度ログインしてトークンを更新する必要があります。これは望ましくありません。アプリケーションセッションのタイムアウト時間がまだ経過していないため、ユーザーは再度ログインするように求めないでください。

ユーザーに再度ログインを強制せずにトークンを更新するにはどうすればよいですか。

以下に示すJWTアクセストークンの実装ロジックが適切でないか、正しくない場合は、正しい方法を教えてください。

以下はコードです。

WebAPI

AuthHandler.cs

  public class AuthHandler : DelegatingHandler
    {

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
                CancellationToken cancellationToken)
    {
        HttpResponseMessage errorResponse = null;           
        try
        {
            IEnumerable<string> authHeaderValues;
            request.Headers.TryGetValues("Authorization", out authHeaderValues);

            if (authHeaderValues == null)
                return base.SendAsync(request, cancellationToken);

            var requestToken = authHeaderValues.ElementAt(0);

            var token = "";

            if (requestToken.StartsWith("Bearer ", StringComparison.CurrentCultureIgnoreCase))
            {
                token = requestToken.Substring("Bearer ".Length);
            }

            var secret = "w$e$#*az";

            ClaimsPrincipal cp = ValidateToken(token, secret, true);


            Thread.CurrentPrincipal = cp;

            if (HttpContext.Current != null)
            {
                Thread.CurrentPrincipal = cp;
                HttpContext.Current.User = cp;
            }
        }
        catch (SignatureVerificationException ex)
        {
            errorResponse = request.CreateErrorResponse(HttpStatusCode.Unauthorized, ex.Message);
        }
        catch (Exception ex)
        {
            errorResponse = request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex.Message);
        }


        return errorResponse != null
            ? Task.FromResult(errorResponse)
            : base.SendAsync(request, cancellationToken);
    }

    private static ClaimsPrincipal ValidateToken(string token, string secret, bool checkExpiration)
    {
        var jsonSerializer = new JavaScriptSerializer();
        string payloadJson = string.Empty;

        try
        {
            payloadJson = JsonWebToken.Decode(token, secret);
        }
        catch (Exception)
        {
            throw new SignatureVerificationException("Unauthorized access!");
        }

        var payloadData = jsonSerializer.Deserialize<Dictionary<string, object>>(payloadJson);


        object exp;
        if (payloadData != null && (checkExpiration && payloadData.TryGetValue("exp", out exp)))
        {
            var validTo = AuthFactory.FromUnixTime(long.Parse(exp.ToString()));
            if (DateTime.Compare(validTo, DateTime.UtcNow) <= 0)
            {
                throw new SignatureVerificationException("Token is expired!");
            }
        }

        var clmsIdentity = new ClaimsIdentity("Federation", ClaimTypes.Name, ClaimTypes.Role);

        var claims = new List<Claim>();

        if (payloadData != null)
            foreach (var pair in payloadData)
            {
                var claimType = pair.Key;

                var source = pair.Value as ArrayList;

                if (source != null)
                {
                    claims.AddRange(from object item in source
                                    select new Claim(claimType, item.ToString(), ClaimValueTypes.String));

                    continue;
                }

                switch (pair.Key.ToUpper())
                {
                    case "USERNAME":
                        claims.Add(new Claim(ClaimTypes.Name, pair.Value.ToString(), ClaimValueTypes.String));
                        break;
                    case "EMAILID":
                        claims.Add(new Claim(ClaimTypes.Email, pair.Value.ToString(), ClaimValueTypes.Email));
                        break;
                    case "USERID":
                        claims.Add(new Claim(ClaimTypes.UserData, pair.Value.ToString(), ClaimValueTypes.Integer));
                        break;
                    default:
                        claims.Add(new Claim(claimType, pair.Value.ToString(), ClaimValueTypes.String));
                        break;
                }
            }

        clmsIdentity.AddClaims(claims);

        ClaimsPrincipal cp = new ClaimsPrincipal(clmsIdentity);

        return cp;
    }


}

AuthFactory.cs

public static class AuthFactory
{
internal static DateTime FromUnixTime(double unixTime)
{
    var Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    return Epoch.AddSeconds(unixTime);
}


internal static string CreateToken(User user, string loginID, out double issuedAt, out double expiryAt)
{

    var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    expiryAt = Math.Round((DateTime.UtcNow.AddMinutes(TokenLifeDuration) - unixEpoch).TotalSeconds);
    issuedAt = Math.Round((DateTime.UtcNow - unixEpoch).TotalSeconds);

    var payload = new Dictionary<string, object>
        {
            {enmUserIdentity.UserName.ToString(), user.Name},
            {enmUserIdentity.EmailID.ToString(), user.Email},
            {enmUserIdentity.UserID.ToString(), user.UserID},
            {enmUserIdentity.LoginID.ToString(), loginID}
            ,{"iat", issuedAt}
            ,{"exp", expiryAt}
        };

    var secret = "w$e$#*az";

    var token = JsonWebToken.Encode(payload, secret, JwtHashAlgorithm.HS256);

    return token;
}

public static int TokenLifeDuration
{
    get
    {
        int tokenLifeDuration = 20; // in minuets
        return tokenLifeDuration;
    }
}

internal static string CreateMasterToken(int userID, string loginID)
{

    var payload = new Dictionary<string, object>
        {
            {enmUserIdentity.LoginID.ToString(), loginID},
            {enmUserIdentity.UserID.ToString(), userID},
            {"instanceid", DateTime.Now.ToFileTime()}
        };

    var secret = "w$e$#*az";

    var token = JsonWebToken.Encode(payload, secret, JwtHashAlgorithm.HS256);

    return token;
}

}

WebApiConfig.cs

public static class WebApiConfig
{

    public static void Register(HttpConfiguration config)
    {
        var cors = new EnableCorsAttribute("*", "*", "*");
        config.EnableCors(cors);

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Formatters.Remove(config.Formatters.XmlFormatter);

        config.MessageHandlers.Add(new AuthHandler());
    }
}

TokenController .cs

public class TokenController : ApiController
{
    [AllowAnonymous]
    [Route("signin")]
    [HttpPost]
    public HttpResponseMessage Login(Login model)
    {
        HttpResponseMessage response = null;
        DataTable dtblLogin = null;
        double issuedAt;
        double expiryAt;

        if (ModelState.IsValid)
        {
            dtblLogin = LoginManager.GetUserLoginDetails(model.LoginID, model.Password, true);

            if (dtblLogin == null || dtblLogin.Rows.Count == 0)
            {
                response = Request.CreateResponse(HttpStatusCode.NotFound);
            }
            else
            {
                User loggedInUser = new User();
                loggedInUser.UserID = Convert.ToInt32(dtblLogin.Rows[0]["UserID"]);
                loggedInUser.Email = Convert.ToString(dtblLogin.Rows[0]["UserEmailID"]);
                loggedInUser.Name = Convert.ToString(dtblLogin.Rows[0]["LastName"]) + " " + Convert.ToString(dtblLogin.Rows[0]["FirstName"]);

                string token = AuthFactory.CreateToken(loggedInUser, model.LoginID, out issuedAt, out expiryAt);
                loggedInUser.Token = token;

                response = Request.CreateResponse(loggedInUser);

            }
        }
        else
        {
            response = Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
        }
        return response;
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
    }

}

PremiumCalculatorController.cs

PremiumCalculatorController : ApiController
{
    [HttpPost]
    public IHttpActionResult CalculatAnnualPremium(PremiumFactorInfo premiumFactDetails)
    {
      PremiumInfo result;
      result = AnnualPremium.GetPremium(premiumFactDetails);
      return Ok(result);
    }
}

Webフォームアプリケーション

Login.aspx.cs

public class Login
{
    protected void imgbtnLogin_Click(object sender, System.EventArgs s)
    {

    UserInfo loggedinUser = LoginManager.ValidateUser(txtUserID.text.trim(), txtPassword.text);

    if (loggedinUser != null)
    {

        byte[] password = LoginManager.EncryptPassword(txtPassword.text);

        APIToken tokenInfo = ApiLoginManager.Login(txtUserID.text.trim(), password);

        loggedinUser.AccessToken = tokenInfo.Token;

        Session.Add("LoggedInUser", loggedinUser);

        Response.Redirect("Home.aspx");

    }
    else
    {
        msg.Show("Logn ID or Password is invalid.");
    }


    }
}

ApiLoginManager.cs

public class ApiLoginManager
{
    public UserDetails Login(string userName, byte[] password)
    {
        APIToken result = null;
        UserLogin objLoginInfo;
        string webAPIBaseURL = "http://localhost/polwebapiService/"
        try
        {
            using (var client = new HttpClient())
            {
                result = new UserDetails();
                client.BaseAddress = new Uri(webAPIBaseURL);
                objLoginInfo = new UserLogin { LoginID = userName, Password = password };

                var response = client.PostAsJsonAsync("api/token/Login", objLoginInfo);

                if (response.Result.IsSuccessStatusCode)
                {
                    string jsonResponce = response.Result.Content.ReadAsStringAsync().Result;
                    result = JsonConvert.DeserializeObject<APIToken>(jsonResponce);
                }

                response = null;
            }

            return result;
        }
        catch (Exception ex)
        {
            throw ex;
        }

    }

}

AnnualPremiumCalculator.aspx.cs

public class AnnualPremiumCalculator
{
    protected void imgbtnCalculatePremium_Click(object sender, System.EventArgs s)
    { 
       string token = ((UserInfo)Session["LoggedInUser"]).AccessToken;
       PremiumFactors premiumFacts = CollectUserInputPremiumFactors();
       PremiumInfo premiumDet = CalculatePremium(premiumFacts, token);
       txtAnnulPremium.text = premiumDet.Premium;
       //other details so on 
    }

    public PremiumInfo CalculatePremium(PremiumFactors premiumFacts, string accessToken)
    {
        PremiumInfo result = null;
        string webAPIBaseURL = "http://localhost/polwebapiService/";
        try
        {
            using (var client = new HttpClient())
            {
                client.BaseAddress = new Uri(webAPIBaseURL);

                StringContent content = new StringContent(JsonConvert.SerializeObject(premiumFacts), Encoding.UTF8, "application/json");

                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

                var response = client.PostAsync("api/calculators/PremiumCalculator", content);

                if (response.Result.IsSuccessStatusCode)
                {
                    string jsonResponce = response.Result.Content.ReadAsStringAsync().Result;
                    result = JsonConvert.DeserializeObject<PremiumInfo>(jsonResponce);
                }

                response = null;

            }

            return result;
        }
        finally
        {

        }

    }

}

上記は問題を説明するためのサンプルコードですが、タイプミスがある可能性があります。

5

私はいくつかの意見があります:

  1. アクセストークンは、サーバー上のセッションではなく、クライアントによって保存されることを目的としています。更新トークンについても同じことが言えます。その理由は、通常はセッションがないためです。スマートクライアントはセッションなしでトークンを処理でき、MVC WebサイトはCookieを使用でき、APIはセッションを認識しません。禁止されているわけではありませんが、セッションの有効期限について心配する必要があり、サーバーを再起動するときにすべてのユーザーが再度ログインする必要があります。

  2. OAuthを実装する場合は、 仕様 を読んでください。そこには、更新トークンを実装するために必要なすべてのものがあります。

  3. TokenControllerでは、ログインを処理します。そこで、 その他の条件 も確認する必要があります。

    • grant_type =パスワード
    • Content-Typeは「application/x-www-form-urlencoded」である必要があります
    • リクエストは、セキュリティで保護された回線(https)を介して送信する場合にのみ処理する必要があります。
  4. Access_tokenが取得され、refresh_tokenが要求された場合にのみ、 refresh_token をaccess_tokenに含める必要があります。

  5. クライアントアプリケーション (grant_type = client_credentials)は、clientid/secretを使用してアクセストークンを取得するため、更新トークンは必要ありません。 TokenControllerを拡張して、client_credentialsフローを許可します。注意:更新トークンはユーザー専用であり、秘密にしておくことができる場合にのみ使用する必要があります。更新トークンは非常に強力なので、取り扱いには注意してください。

  6. アクセストークンを更新 するには、更新トークンをエンドポイントに送信する必要があります。あなたの場合、TokenControllerを拡張してrefresh_tokenリクエストを許可することができます。以下を確認する必要があります。

    • grant_type = refresh_token
    • Content-Typeは「application/x-www-form-urlencoded」である必要があります
  7. 更新トークンにはいくつかのシナリオがあり、それらを組み合わせることもできます。

    • 更新トークンをデータベースに保存します。更新トークンを使用するたびに、データベースから削除してから、新しいaccess_tokenに返される新しい更新トークンを保存できます。
    • 更新トークンをより長い有効期間に設定し、アクセストークンが更新されたときに更新しないでください。この場合、返されるaccess_tokenには新しい更新トークンが含まれていません。そうすれば、refresh_tokenの有効期限が切れた後に再度ログインする必要があります。
  8. 有効期限がなく、取り消すことのできない更新トークンは、ユーザーに無制限のアクセスを提供するため、実装には注意してください。

  9. 私の答えでは ここ ID2を使用して更新トークンを処理する方法を確認できます。Identity2への切り替えを検討できます。

私はすべてに言及したと思います。私が何かを逃したか、何かがはっきりしないならば、私に知らせてください。

5

これは、個別の永続的な更新トークンを使用して実行できます。 http://www.c-sharpcorner.com/article/handle-refresh-token-using-asp-net-core-2-0-and-json-web-token/ の素敵なチュートリアル==

5
Niko