Skip to content
This repository has been archived by the owner on Jan 24, 2021. It is now read-only.

Commit

Permalink
Token authentication and authorization implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffdoolittle committed Mar 17, 2014
1 parent e68ea60 commit 9ae0a54
Show file tree
Hide file tree
Showing 30 changed files with 2,251 additions and 0 deletions.
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{3C131D45-AF1D-4659-8B26-A9F55EED0D20}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Nancy.Authentication.Token.Tests</RootNamespace>
<AssemblyName>Nancy.Authentication.Token.Tests</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'MonoDebug|AnyCPU'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\MonoDebug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'MonoRelease|AnyCPU'">
<OutputPath>bin\MonoRelease\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="FakeItEasy, Version=1.14.0.0, Culture=neutral, PublicKeyToken=eff28e2146d5fd2c, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\FakeItEasy.1.14.0\lib\net40\FakeItEasy.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="xunit, Version=1.9.1.1600, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\xunit.1.9.1\lib\net20\xunit.dll</HintPath>
</Reference>
<Reference Include="xunit.extensions, Version=1.9.1.1600, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\xunit.extensions.1.9.1\lib\net20\xunit.extensions.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Nancy.Tests\Fakes\FakeRequest.cs">
<Link>Fakes\FakeRequest.cs</Link>
</Compile>
<Compile Include="..\Nancy.Tests\ShouldExtensions.cs">
<Link>ShouldExtensions.cs</Link>
</Compile>
<Compile Include="..\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Storage\FileSystemTokenKeyStoreFixture.cs" />
<Compile Include="TokenAuthenticationConfigurationFixture.cs" />
<Compile Include="TokenAuthenticationFixture.cs" />
<Compile Include="TokenizerFixture.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Nancy.Authentication.Token\Nancy.Authentication.Token.csproj">
<Project>{97fa024a-f6ed-4086-bcc1-1a51be63474c}</Project>
<Name>Nancy.Authentication.Token</Name>
</ProjectReference>
<ProjectReference Include="..\Nancy.Testing\Nancy.Testing.csproj">
<Project>{D79203C0-B672-4751-9C95-C3AB7D3FEFBE}</Project>
<Name>Nancy.Testing</Name>
</ProjectReference>
<ProjectReference Include="..\Nancy\Nancy.csproj">
<Project>{34576216-0dca-4b0f-a0dc-9075e75a676f}</Project>
<Name>Nancy</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
@@ -0,0 +1,55 @@
using System;

namespace Nancy.Authentication.Token.Tests.Storage
{
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Nancy.Authentication.Token.Storage;
using Nancy.Testing.Fakes;
using Xunit;

public class FileSystemTokenKeyStoreFixture : IDisposable
{
private FileSystemTokenKeyStore keyStore;

public FileSystemTokenKeyStoreFixture()
{
var rootPathProvider = new FakeRootPathProvider();
this.keyStore = new FileSystemTokenKeyStore(rootPathProvider);
}

[Fact]
public void Should_store_keys_in_file()
{
var keys = new Dictionary<DateTime, byte[]>();

keys.Add(DateTime.UtcNow, Encoding.UTF8.GetBytes("fake encryption key"));

keyStore.Store(keys);

Assert.True(File.Exists(keyStore.FilePath));
}

[Fact]
public void Should_retrieve_keys_from_file()
{
var keys = new Dictionary<DateTime, byte[]>();

keys.Add(DateTime.UtcNow, Encoding.UTF8.GetBytes("fake encryption key"));

keyStore.Store(keys);

var retrievedKeys = keyStore.Retrieve();

Assert.True(Encoding.UTF8.GetString(retrievedKeys.Values.First())
== "fake encryption key");
}

public void Dispose()
{
keyStore.Purge();
}
}
}
@@ -0,0 +1,17 @@
namespace Nancy.Authentication.Token.Tests
{
using System;
using Nancy.Tests;
using Xunit;

public class TokenAuthenticationConfigurationFixture
{
[Fact]
public void Should_throw_with_null_tokenizer()
{
var result = Record.Exception(() => new TokenAuthenticationConfiguration(null));

result.ShouldBeOfType(typeof (ArgumentException));
}
}
}
166 changes: 166 additions & 0 deletions src/Nancy.Authentication.Token.Tests/TokenAuthenticationFixture.cs
@@ -0,0 +1,166 @@
namespace Nancy.Authentication.Token.Tests
{
using System;
using System.Collections.Generic;
using System.Threading;
using FakeItEasy;

using Nancy.Security;
using Nancy.Tests;
using Nancy.Bootstrapper;
using Nancy.Tests.Fakes;
using Xunit;

public class TokenAuthenticationFixture
{
private readonly TokenAuthenticationConfiguration config;
private readonly IPipelines hooks;

public TokenAuthenticationFixture()
{
this.config = new TokenAuthenticationConfiguration(A.Fake<ITokenizer>());
this.hooks = new Pipelines();
TokenAuthentication.Enable(this.hooks, this.config);
}

[Fact]
public void Should_add_a_pre_hook_in_application_when_enabled()
{
// Given
var pipelines = A.Fake<IPipelines>();

// When
TokenAuthentication.Enable(pipelines, this.config);

// Then
A.CallTo(() => pipelines.BeforeRequest.AddItemToStartOfPipeline(A<Func<NancyContext, Response>>.Ignored))
.MustHaveHappened(Repeated.Exactly.Once);
}

[Fact]
public void Should_add_both_token_and_requires_auth_pre_hook_in_module_when_enabled()
{
// Given
var module = new FakeModule();

// When
TokenAuthentication.Enable(module, this.config);

// Then
module.Before.PipelineDelegates.ShouldHaveCount(2);
}

[Fact]
public void Should_throw_with_null_config_passed_to_enable_with_application()
{
// Given, When
var result = Record.Exception(() => TokenAuthentication.Enable(A.Fake<IPipelines>(), null));

// Then
result.ShouldBeOfType(typeof(ArgumentNullException));
}

[Fact]
public void Should_throw_with_null_config_passed_to_enable_with_module()
{
// Given, When
var result = Record.Exception(() => TokenAuthentication.Enable(new FakeModule(), null));

// Then
result.ShouldBeOfType(typeof(ArgumentNullException));
}

[Fact]
public void Pre_request_hook_should_not_set_auth_details_with_no_auth_headers()
{
// Given
var context = new NancyContext()
{
Request = new FakeRequest("GET", "/")
};

// When
var result = this.hooks.BeforeRequest.Invoke(context, new CancellationToken());

// Then
result.Result.ShouldBeNull();
context.CurrentUser.ShouldBeNull();
}

[Fact]
public void Pre_request_hook_should_not_set_auth_details_when_invalid_scheme_in_auth_header()
{
// Given
var context = CreateContextWithHeader(
"Authorization", new[] { "FooScheme" + " " + "A-FAKE-TOKEN" });

// When
var result = this.hooks.BeforeRequest.Invoke(context, new CancellationToken());

// Then
result.Result.ShouldBeNull();
context.CurrentUser.ShouldBeNull();
}

[Fact]
public void Pre_request_hook_should_call_tokenizer_with_token_in_auth_header()
{
// Given
var context = CreateContextWithHeader(
"Authorization", new[] { "Token" + " " + "mytoken" });

// When
this.hooks.BeforeRequest.Invoke(context, new CancellationToken());

// Then
A.CallTo(() => config.Tokenizer.Detokenize("mytoken", context)).MustHaveHappened();
}

[Fact]
public void Should_set_user_in_context_with_valid_username_in_auth_header()
{
// Given
var fakePipelines = new Pipelines();

var context = CreateContextWithHeader(
"Authorization", new[] { "Token" + " " + "mytoken" });

var tokenizer = A.Fake<ITokenizer>();
var fakeUser = A.Fake<IUserIdentity>();
A.CallTo(() => tokenizer.Detokenize("mytoken", context)).Returns(fakeUser);

var cfg = new TokenAuthenticationConfiguration(tokenizer);

TokenAuthentication.Enable(fakePipelines, cfg);

// When
fakePipelines.BeforeRequest.Invoke(context, new CancellationToken());

// Then
context.CurrentUser.ShouldBeSameAs(fakeUser);
}

private static NancyContext CreateContextWithHeader(string name, IEnumerable<string> values)
{
var header = new Dictionary<string, IEnumerable<string>>
{
{ name, values }
};

return new NancyContext()
{
Request = new FakeRequest("GET", "/", header)
};
}

class FakeModule : NancyModule
{
public FakeModule()
{
this.After = new AfterPipeline();
this.Before = new BeforePipeline();
this.OnError = new ErrorPipeline();
}
}
}
}

23 comments on commit 9ae0a54

@jeffdoolittle
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that this PR has been merged, I realize there would be a benefit to adding a BrowserContextExtension in Nancy.Testing for TokenAuth. However, this would require Nancy.Testing taking a dependency on Nancy.Authentication.Token.

@grumpydev @thecodejunkie let me know if you have objections to Nancy.Testing taking this dependency. If not, I'll go ahead and implement the Extension and send in another PR.

--Jeff

@grumpydev
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm not sure we would want that dependency, it would either have to handle loading it dynamically, or be a separate testing thing.

We've had similar issues with forms auth testing - not sure what the best solution is.

@jeffdoolittle
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem. It's fairly trivial to just fake up my own extension method in my own code.

@thecodejunkie
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the FileSystemTokenKeyStoreFixture tests needs some love. They're somewhat brittle. Every now and then the Should_retrieve_keys_from_file tests fail with

System.IO.IOException
The directory is not empty.

The issue is probably because the tests, in the fixture, share a keystore instance and the purging can get messed up.

@jeffdoolittle
Copy link
Contributor Author

@jeffdoolittle jeffdoolittle commented on 9ae0a54 Mar 30, 2014 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thecodejunkie
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just pushed an update to the tests - would be ace if you gave them a quick look as well =)

@jeffdoolittle
Copy link
Contributor Author

@jeffdoolittle jeffdoolittle commented on 9ae0a54 Mar 30, 2014 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grumpydev
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does do that - unless you have statics there's no "state" issues between tests.

@dhrobbins
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on how you can revoke a token? The scenario is that an admin removes user from database, yet the token hasn't expired. If the token has an expiry of 24 hours, then there is window where the user could still access site.

I've tried setting up a registry where I record the token issue with the user, but I see only 1 token being recorded in the keyChain.bin file.

@jeffdoolittle
Copy link
Contributor Author

@jeffdoolittle jeffdoolittle commented on 9ae0a54 Sep 1, 2014 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dhrobbins
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the great explanation - I hadn't grasped that key to the token generation was stored. Makes sense now.

I had also thought of the "blacklist" approach as well. Could very well be a simple list that is checked on each request like you suggest, and perhaps a purge on regular basis that compares to the active user list. In a sense it will dictated by what the security required by the business. Unfortunately, there are cases where you need to lock down access immediately. But I agree that those scenarios are best handled other mechanisms and this keeps the Token solution cleaner.

@dhrobbins
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought regarding documentation - your explanation is really and maybe could included as "Further detail" link for a discussion of the mechanics.

@jeffdoolittle
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dhrobbins
Copy link

@dhrobbins dhrobbins commented on 9ae0a54 Sep 4, 2014 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thecodemonkey
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey guys, i have one question! What's about load balancing?
And why the generation-key can't be static stored in the web.config or something like that?

best regards

@jeffdoolittle
Copy link
Contributor Author

@jeffdoolittle jeffdoolittle commented on 9ae0a54 Sep 24, 2014 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thecodemonkey
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello Jeff, i like your Security first way. But i think the problem is, that if you application ist so small, that you doesnt need any fail over mechanism, then you dont need to be afraid of any hacker atacks ;-) If you armed for brute force attack, then i could attack you by DDoS. Ok via DDoS i can't stealing any Data, but i can produce a lot of damage. However, what i mean ist: there are a lot of Security reasons, an the solution shuld solve more then only one Problem ;-)

The simplest and most usable load balancing scenario ist:

Load Balancer (balancing by round robin, not sticky sesstion!)
|
|-------- Web Server1 (KeyChain.bin with random generated keys: AAA..., BBB...)
|-------- Web Server2 (KeyChain.bin with random generated keys: CCC..., DDD...)
|-------- Web Server3 (KeyChain.bin with random generated keys: EEE..., FFF...)

With the existing solution, the keys will be generated on each webserver, and will be stored localy on each webserver. If you login on "Web Server1" and Load balancer forwards you on the next Request to "Web Server2" then you AuthTicket cannot be Decrypted, because of different Key, wich ist loaded from localy KeyChain.bin!!!

This solution is not realy Stateless, and this ist my beggest Problem! REST Full Services and JWT are designed to be stateless!

I've wrote a simple JWTTokenizer. It uses the john sheehans nice lightwight solution: https://github.com/johnsheehan/jwt . If someone have the same problem as me, here is my very simple Solution:

a simple JWTTokenizer with shared secret key stored in the Web.config

using JWT;
using Lex.Domain.NancyFx.Errors;
using Nancy;
using Nancy.Authentication.Token;
using Nancy.Security;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Lex.Domain.NancyFx.Authentication
{
    public class JWTTokenizer : ITokenizer
    {
        private string _sharedSecret;
        private int _expirationTimeInMs;

        public JWTTokenizer(string sharedSecret, int expirationTimeInMs = 86400000) 
        {
            this._sharedSecret = sharedSecret;
            this._expirationTimeInMs = expirationTimeInMs;
        }

        public string Tokenize(IUserIdentity userIdentity, NancyContext context)
        {
            var payload = new Dictionary<string, object>() {
                { "usr", userIdentity.UserName },
                { "clms", string.Join("|", userIdentity.Claims) },
                { "expdtm", DateTime.Now.AddMilliseconds(this._expirationTimeInMs).Ticks  }
            };

            return JsonWebToken.Encode(payload, _sharedSecret, JWT.JwtHashAlgorithm.HS256);
        }

        public IUserIdentity Detokenize(string token, NancyContext context)
        {
            try
            {
                var payload = JsonWebToken.DecodeToObject(token, _sharedSecret) as IDictionary<string, object>;
                var expirationTime = new DateTime((long)payload["expdtm"]);

                if (DateTime.Now > expirationTime)
                    throw new TokenExpiredException();

                var claims = payload["clms"].ToString().Split(new char[]{'|'});
                var userIdentity = new TokenUserIdentity(payload["usr"] as string, claims);

                return userIdentity;
            }
            catch (TokenExpiredException exp)
            {
                throw exp;
            }
            catch
            {
                throw new TokenInvalidException();
            }
        }

        private class TokenUserIdentity : IUserIdentity
        {
            public TokenUserIdentity(string userName, IEnumerable<string> claims)
            {
                this.UserName = userName;
                this.Claims = claims;
            }

            public string UserName { get; private set; }
            public IEnumerable<string> Claims { get; private set; }
        }
    }
}

Injection and configuration

Only what you need is to inject this JWTTokenizer, instead of default Tokenizer within you Bootstrap class:

    protected override void ConfigureRequestContainer(
        TinyIoCContainer container, 
        NancyContext context)
    {
        container.Register<ITokenizer>(new JWTTokenizer(ConfigurationManager.AppSettings["shared_secret"]));
        base.ConfigureRequestContainer(container, context);
    }

in the future

For the future i think it should be the combination of your existing solution an Shared_Secret_Key Solution. In the class TokenKeyRing within Tokenizer.cs you could implement something like:

        private byte[] CreateKey(ISharedSecretKeyProvider keyProvider)
        {
            //var secretKey = new byte[64];
            //using (var rng = new RNGCryptoServiceProvider())
            //{
            //    rng.GetBytes(secretKey);
            //}

            return keyProvider.Create();
        }

your existing implementation of random Key generation

And then you could provide some different implementations of ISharedSecredKeyProvider. For Example your already existing implementation:

public class RandomSharedSecredProvider :  ISharedSecretKeyProvider  {

        private byte[] CreateKey(ISharedSecretKeyProvider keyProvider)
        {
            var secretKey = new byte[64];
            using (var rng = new RNGCryptoServiceProvider())
            {
                rng.GetBytes(secretKey);
            }

            return secretKey;
        }
}

one of possible implementations

Or my favorite implementation would be this:

public class MongoDBSharedSecredProvider :  ISharedSecretKeyProvider  {

        private string _connectionstring;

        private MongoDBSharedSecredProvider(string connectionString)
        {
              this._connectionstring = connectionString;
        }

        private byte[] Create()
        {
              var client = new MongoClient(this._connectionstring);
              var server = client.GetServer();
              var database = server.GetDatabase("myDB");
              var keys = database.GetCollection<SharedSecretKey>("keys");

              var result =
                    (from u in keys.AsQueryable<SharedSecretKey>()
                     select u.Key)
                    .First();

              return System.Text.ASCIIEncoding.ASCII.GetBytes(result);
        }
}

there are a lot of challanges like expiration an synchronisation of keys across multiple webservers. But i think it could be the possible way, why not!

sorry for my bad english!

best regards
Ilja

@vincentparrett
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd certainly like to see some way to invalidate a token. I don't see a way to do this without keeping a list of user/token(s) and then perhaps a blacklist for revoked but not expired tokens.

I'm leaning towards using the in memory keystore, so I guess the user/tokens & blacklist could be in memory too.. I guess that depends on how many users you expect to be logged in.

@dhrobbins
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding invalidating the token, @jeffdoolittle mentioned that dropping the token on the client side essentially eliminates the token for that user. If you kept a black list of the new people to reject then you could redirect to a "Get lost" page that then purges the localStorage / cookie from the client.

Basically you would have to check the list for each transaction between the client and web server. If it's small enough you could just cache it. Clearly you would have to purge this list periodically, but you'd need to ensure that the client side token purge has occurred. On the bright side of things after the token expires, the "Get lost LUser" is locked out.

Another idea would be to create a claim of "Locked Out" and in the entry point of the Modules, test for that claim, redirect to "Get Lost" page, purge on the client, etc. This way you could just record the "Locked Claim" in the database / document and not have to maintain a list in memory, and purge the list. Simply mark the user as "Locked Out", then after the expiry period you can delete the user out of the system, and they won't have access anyway.

@netinhoteixeira
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello,

Exists a way to force re-login? Or force to expire a Token?

Thank you!

@gzpbx
Copy link

@gzpbx gzpbx commented on 9ae0a54 Feb 16, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thecodemonkey thanks, I encounter the load balancing issue too. #2279

@chenzuo
Copy link

@chenzuo chenzuo commented on 9ae0a54 Nov 26, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where are keyChain.bin? i can't find it on my computer

@chenzuo
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dhrobbins
Hey guys, Have you solved the problem of expiring keys immediately? Can you give a solution to the problem?

Please sign in to comment.