2019-07-01 05:39:25 +02:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Net ;
using System.Threading.Tasks ;
2019-10-05 20:44:12 +09:00
using System.Security.Claims ;
2019-07-01 05:39:25 +02:00
using BTCPayServer.Tests.Logging ;
2019-10-08 15:21:30 +09:00
using Microsoft.AspNetCore.Authentication.OpenIdConnect ;
2019-07-01 05:39:25 +02:00
using Microsoft.IdentityModel.Protocols.OpenIdConnect ;
using Xunit ;
using Xunit.Abstractions ;
using System.Net.Http ;
using System.Net.Http.Headers ;
2019-09-29 09:23:31 +02:00
using BTCPayServer.Authentication ;
2019-07-01 05:39:25 +02:00
using BTCPayServer.Data ;
using Newtonsoft.Json ;
using Newtonsoft.Json.Linq ;
using OpenIddict.Abstractions ;
using OpenQA.Selenium ;
2019-10-12 20:35:30 +09:00
using Microsoft.AspNetCore.Identity ;
2019-07-01 05:39:25 +02:00
namespace BTCPayServer.Tests
{
public class AuthenticationTests
{
2019-10-08 15:21:30 +09:00
public const int TestTimeout = TestUtils . TestTimeout ;
2019-09-11 16:22:41 +09:00
public AuthenticationTests ( ITestOutputHelper helper )
2019-07-01 05:39:25 +02:00
{
2019-09-11 16:22:41 +09:00
Logs . Tester = new XUnitLog ( helper ) { Name = "Tests" } ;
2019-07-01 05:39:25 +02:00
Logs . LogProvider = new XUnitLogProvider ( helper ) ;
}
2019-10-10 16:43:38 +09:00
[Fact(Timeout = TestTimeout)]
2019-07-01 05:39:25 +02:00
[Trait("Integration", "Integration")]
public async Task GetRedirectedToLoginPathOnChallenge ( )
{
using ( var tester = ServerTester . Create ( ) )
{
2019-10-07 16:04:25 +09:00
await tester . StartAsync ( ) ;
2019-07-01 05:39:25 +02:00
var client = tester . PayTester . HttpClient ;
//Wallets endpoint is protected
var response = await client . GetAsync ( "wallets" ) ;
var urlPath = response . RequestMessage . RequestUri . ToString ( )
. Replace ( tester . PayTester . ServerUri . ToString ( ) , "" ) ;
//Cookie Challenge redirects you to login page
Assert . StartsWith ( "Account/Login" , urlPath , StringComparison . InvariantCultureIgnoreCase ) ;
var queryString = response . RequestMessage . RequestUri . ParseQueryString ( ) ;
Assert . NotNull ( queryString [ "ReturnUrl" ] ) ;
Assert . Equal ( "/wallets" , queryString [ "ReturnUrl" ] ) ;
}
}
2019-10-10 16:43:38 +09:00
[Fact(Timeout = TestTimeout)]
2019-07-01 05:39:25 +02:00
[Trait("Integration", "Integration")]
public async Task CanGetOpenIdConfiguration ( )
{
using ( var tester = ServerTester . Create ( ) )
{
2019-10-07 16:04:25 +09:00
await tester . StartAsync ( ) ;
2019-07-01 05:39:25 +02:00
using ( var response =
await tester . PayTester . HttpClient . GetAsync ( "/.well-known/openid-configuration" ) )
{
using ( var streamToReadFrom = new StreamReader ( await response . Content . ReadAsStreamAsync ( ) ) )
{
var json = await streamToReadFrom . ReadToEndAsync ( ) ;
Assert . NotNull ( json ) ;
var configuration = OpenIdConnectConfiguration . Create ( json ) ;
Assert . NotNull ( configuration ) ;
}
}
}
}
2019-10-10 16:43:38 +09:00
[Fact(Timeout = TestTimeout)]
2019-07-01 05:39:25 +02:00
[Trait("Integration", "Integration")]
public async Task CanUseNonInteractiveFlows ( )
{
using ( var tester = ServerTester . Create ( ) )
{
2019-10-07 16:04:25 +09:00
await tester . StartAsync ( ) ;
2019-07-01 05:39:25 +02:00
var user = tester . NewAccount ( ) ;
user . GrantAccess ( ) ;
2019-10-12 20:35:30 +09:00
await user . MakeAdmin ( ) ;
2019-07-01 05:39:25 +02:00
var token = await RegisterPasswordClientAndGetAccessToken ( user , null , tester ) ;
await TestApiAgainstAccessToken ( token , tester , user ) ;
token = await RegisterPasswordClientAndGetAccessToken ( user , "secret" , tester ) ;
await TestApiAgainstAccessToken ( token , tester , user ) ;
token = await RegisterClientCredentialsFlowAndGetAccessToken ( user , "secret" , tester ) ;
await TestApiAgainstAccessToken ( token , tester , user ) ;
}
}
2019-10-10 16:43:38 +09:00
[Fact(Timeout = TestTimeout)]
2019-07-01 05:39:25 +02:00
[Trait("Selenium", "Selenium")]
public async Task CanUseImplicitFlow ( )
{
2019-09-11 16:22:41 +09:00
using ( var s = SeleniumTester . Create ( ) )
{
2019-10-07 16:04:25 +09:00
await s . StartAsync ( ) ;
2019-09-11 16:22:41 +09:00
var tester = s . Server ;
2019-07-01 05:39:25 +02:00
2019-09-11 16:22:41 +09:00
var user = tester . NewAccount ( ) ;
user . GrantAccess ( ) ;
2019-10-08 15:21:30 +09:00
await user . MakeAdmin ( ) ;
2019-09-11 16:22:41 +09:00
var id = Guid . NewGuid ( ) . ToString ( ) ;
var redirecturi = new Uri ( "http://127.0.0.1/oidc-callback" ) ;
var openIdClient = await user . RegisterOpenIdClient (
new OpenIddictApplicationDescriptor ( )
{
ClientId = id ,
DisplayName = id ,
Permissions = { OpenIddictConstants . Permissions . GrantTypes . Implicit } ,
2019-09-29 09:23:31 +02:00
RedirectUris = { redirecturi } ,
2019-09-11 16:22:41 +09:00
} ) ;
var implicitAuthorizeUrl = new Uri ( tester . PayTester . ServerUri ,
2019-10-08 15:21:30 +09:00
$"connect/authorize?response_type=token&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid server_management store_management&nonce={Guid.NewGuid().ToString()}" ) ;
2019-09-11 16:22:41 +09:00
s . Driver . Navigate ( ) . GoToUrl ( implicitAuthorizeUrl ) ;
s . Login ( user . RegisterDetails . Email , user . RegisterDetails . Password ) ;
s . Driver . FindElement ( By . Id ( "consent-yes" ) ) . Click ( ) ;
var url = s . Driver . Url ;
var results = url . Split ( "#" ) . Last ( ) . Split ( "&" )
. ToDictionary ( s1 = > s1 . Split ( "=" ) [ 0 ] , s1 = > s1 . Split ( "=" ) [ 1 ] ) ;
await TestApiAgainstAccessToken ( results [ "access_token" ] , tester , user ) ;
//in Implicit mode, you renew your token by hitting the same endpoint but adding prompt=none. If you are still logged in on the site, you will receive a fresh token.
var implicitAuthorizeUrlSilentModel = new Uri ( $"{implicitAuthorizeUrl.OriginalString}&prompt=none" ) ;
s . Driver . Navigate ( ) . GoToUrl ( implicitAuthorizeUrlSilentModel ) ;
url = s . Driver . Url ;
results = url . Split ( "#" ) . Last ( ) . Split ( "&" ) . ToDictionary ( s1 = > s1 . Split ( "=" ) [ 0 ] , s1 = > s1 . Split ( "=" ) [ 1 ] ) ;
await TestApiAgainstAccessToken ( results [ "access_token" ] , tester , user ) ;
2019-10-18 23:42:06 +09:00
var stores = await TestApiAgainstAccessToken < StoreData [ ] > ( results [ "access_token" ] ,
$"api/test/me/stores" ,
tester . PayTester . HttpClient ) ;
Assert . NotEmpty ( stores ) ;
Assert . True ( await TestApiAgainstAccessToken < bool > ( results [ "access_token" ] ,
$"api/test/me/stores/{stores[0].Id}/can-edit" ,
tester . PayTester . HttpClient ) ) ;
2019-09-29 09:23:31 +02:00
//we dont ask for consent after acquiring it the first time for the same scopes.
2019-10-18 23:42:06 +09:00
LogoutFlow ( tester , id , s ) ;
2019-09-11 16:22:41 +09:00
s . Driver . Navigate ( ) . GoToUrl ( implicitAuthorizeUrl ) ;
s . Login ( user . RegisterDetails . Email , user . RegisterDetails . Password ) ;
2019-10-18 23:42:06 +09:00
s . Driver . AssertElementNotFound ( By . Id ( "consent-yes" ) ) ;
2019-09-29 09:23:31 +02:00
2019-10-18 23:42:06 +09:00
// Let's asks without scopes
LogoutFlow ( tester , id , s ) ;
id = Guid . NewGuid ( ) . ToString ( ) ;
openIdClient = await user . RegisterOpenIdClient (
new OpenIddictApplicationDescriptor ( )
{
ClientId = id ,
DisplayName = id ,
Permissions = { OpenIddictConstants . Permissions . GrantTypes . Implicit } ,
RedirectUris = { redirecturi } ,
} ) ;
implicitAuthorizeUrl = new Uri ( tester . PayTester . ServerUri ,
$"connect/authorize?response_type=token&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid&nonce={Guid.NewGuid().ToString()}" ) ;
2019-09-29 09:23:31 +02:00
s . Driver . Navigate ( ) . GoToUrl ( implicitAuthorizeUrl ) ;
2019-10-18 23:42:06 +09:00
s . Login ( user . RegisterDetails . Email , user . RegisterDetails . Password ) ;
2019-09-29 09:23:31 +02:00
s . Driver . FindElement ( By . Id ( "consent-yes" ) ) . Click ( ) ;
2019-10-18 23:42:06 +09:00
results = s . Driver . Url . Split ( "#" ) . Last ( ) . Split ( "&" )
2019-09-29 09:23:31 +02:00
. ToDictionary ( s1 = > s1 . Split ( "=" ) [ 0 ] , s1 = > s1 . Split ( "=" ) [ 1 ] ) ;
await Assert . ThrowsAnyAsync < HttpRequestException > ( async ( ) = >
{
2019-10-18 23:42:06 +09:00
await TestApiAgainstAccessToken < StoreData [ ] > ( results [ "access_token" ] ,
$"api/test/me/stores" ,
tester . PayTester . HttpClient ) ;
2019-09-29 09:23:31 +02:00
} ) ;
await Assert . ThrowsAnyAsync < HttpRequestException > ( async ( ) = >
{
await TestApiAgainstAccessToken < bool > ( results [ "access_token" ] ,
2019-10-18 23:42:06 +09:00
$"api/test/me/stores/{stores[0].Id}/can-edit" ,
tester . PayTester . HttpClient ) ;
2019-09-29 09:23:31 +02:00
} ) ;
2019-09-11 16:22:41 +09:00
}
2019-07-01 05:39:25 +02:00
}
2019-07-04 21:18:16 +09:00
void LogoutFlow ( ServerTester tester , string clientId , SeleniumTester seleniumTester )
2019-07-01 05:39:25 +02:00
{
var logoutUrl = new Uri ( tester . PayTester . ServerUri ,
$"connect/logout?response_type=token&client_id={clientId}" ) ;
seleniumTester . Driver . Navigate ( ) . GoToUrl ( logoutUrl ) ;
seleniumTester . GoToHome ( ) ;
Assert . Throws < NoSuchElementException > ( ( ) = > seleniumTester . Driver . FindElement ( By . Id ( "Logout" ) ) ) ;
2019-09-11 16:22:41 +09:00
2019-07-01 05:39:25 +02:00
}
2019-10-10 16:43:38 +09:00
[Fact(Timeout = TestTimeout)]
2019-07-01 05:39:25 +02:00
[Trait("Selenium", "Selenium")]
public async Task CanUseCodeFlow ( )
{
2019-09-11 16:22:41 +09:00
using ( var s = SeleniumTester . Create ( ) )
{
2019-10-07 16:04:25 +09:00
await s . StartAsync ( ) ;
2019-09-11 16:22:41 +09:00
var tester = s . Server ;
2019-07-01 05:39:25 +02:00
2019-09-11 16:22:41 +09:00
var user = tester . NewAccount ( ) ;
user . GrantAccess ( ) ;
2019-10-12 20:35:30 +09:00
await user . MakeAdmin ( ) ;
2019-09-11 16:22:41 +09:00
var id = Guid . NewGuid ( ) . ToString ( ) ;
var redirecturi = new Uri ( "http://127.0.0.1/oidc-callback" ) ;
var secret = "secret" ;
var openIdClient = await user . RegisterOpenIdClient (
new OpenIddictApplicationDescriptor ( )
2019-07-01 05:39:25 +02:00
{
2019-09-11 16:22:41 +09:00
ClientId = id ,
DisplayName = id ,
Permissions =
{
2019-07-01 05:39:25 +02:00
OpenIddictConstants . Permissions . GrantTypes . AuthorizationCode ,
OpenIddictConstants . Permissions . GrantTypes . RefreshToken
2019-09-11 16:22:41 +09:00
} ,
RedirectUris = { redirecturi }
} , secret ) ;
var authorizeUrl = new Uri ( tester . PayTester . ServerUri ,
2019-10-08 15:21:30 +09:00
$"connect/authorize?response_type=code&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid offline_access server_management store_management&state={Guid.NewGuid().ToString()}" ) ;
2019-09-11 16:22:41 +09:00
s . Driver . Navigate ( ) . GoToUrl ( authorizeUrl ) ;
s . Login ( user . RegisterDetails . Email , user . RegisterDetails . Password ) ;
s . Driver . FindElement ( By . Id ( "consent-yes" ) ) . Click ( ) ;
var url = s . Driver . Url ;
var results = url . Split ( "?" ) . Last ( ) . Split ( "&" )
. ToDictionary ( s1 = > s1 . Split ( "=" ) [ 0 ] , s1 = > s1 . Split ( "=" ) [ 1 ] ) ;
var httpClient = tester . PayTester . HttpClient ;
var httpRequest = new HttpRequestMessage ( HttpMethod . Post ,
new Uri ( tester . PayTester . ServerUri , "/connect/token" ) )
{
Content = new FormUrlEncodedContent ( new List < KeyValuePair < string , string > > ( )
2019-07-01 05:39:25 +02:00
{
new KeyValuePair < string , string > ( "grant_type" ,
OpenIddictConstants . GrantTypes . AuthorizationCode ) ,
new KeyValuePair < string , string > ( "client_id" , openIdClient . ClientId ) ,
new KeyValuePair < string , string > ( "client_secret" , secret ) ,
new KeyValuePair < string , string > ( "code" , results [ "code" ] ) ,
new KeyValuePair < string , string > ( "redirect_uri" , redirecturi . AbsoluteUri )
} )
2019-09-11 16:22:41 +09:00
} ;
2019-07-01 05:39:25 +02:00
2019-09-11 16:22:41 +09:00
var response = await httpClient . SendAsync ( httpRequest ) ;
2019-07-01 05:39:25 +02:00
2019-09-11 16:22:41 +09:00
Assert . True ( response . IsSuccessStatusCode ) ;
2019-07-01 05:39:25 +02:00
2019-09-11 16:22:41 +09:00
string content = await response . Content . ReadAsStringAsync ( ) ;
2019-10-05 20:44:12 +09:00
var result = JObject . Parse ( content ) . ToObject < OpenIddictResponse > ( ) ;
2019-07-01 05:39:25 +02:00
2019-09-11 16:22:41 +09:00
await TestApiAgainstAccessToken ( result . AccessToken , tester , user ) ;
2019-07-01 05:39:25 +02:00
2019-09-11 16:22:41 +09:00
var refreshedAccessToken = await RefreshAnAccessToken ( result . RefreshToken , httpClient , id , secret ) ;
2019-09-11 15:36:12 +09:00
2019-09-11 16:22:41 +09:00
await TestApiAgainstAccessToken ( refreshedAccessToken , tester , user ) ;
LogoutFlow ( tester , id , s ) ;
s . Driver . Navigate ( ) . GoToUrl ( authorizeUrl ) ;
s . Login ( user . RegisterDetails . Email , user . RegisterDetails . Password ) ;
Assert . Throws < NoSuchElementException > ( ( ) = > s . Driver . FindElement ( By . Id ( "consent-yes" ) ) ) ;
results = url . Split ( "?" ) . Last ( ) . Split ( "&" )
. ToDictionary ( s1 = > s1 . Split ( "=" ) [ 0 ] , s1 = > s1 . Split ( "=" ) [ 1 ] ) ;
Assert . True ( results . ContainsKey ( "code" ) ) ;
}
2019-07-01 05:39:25 +02:00
}
private static async Task < string > RefreshAnAccessToken ( string refreshToken , HttpClient client , string clientId ,
string clientSecret = null )
{
var httpRequest = new HttpRequestMessage ( HttpMethod . Post ,
new Uri ( client . BaseAddress , "/connect/token" ) )
{
Content = new FormUrlEncodedContent ( new List < KeyValuePair < string , string > > ( )
{
new KeyValuePair < string , string > ( "grant_type" ,
OpenIddictConstants . GrantTypes . RefreshToken ) ,
new KeyValuePair < string , string > ( "client_id" , clientId ) ,
new KeyValuePair < string , string > ( "client_secret" , clientSecret ) ,
new KeyValuePair < string , string > ( "refresh_token" , refreshToken )
} )
} ;
var response = await client . SendAsync ( httpRequest ) ;
Assert . True ( response . IsSuccessStatusCode ) ;
string content = await response . Content . ReadAsStringAsync ( ) ;
2019-10-05 20:44:12 +09:00
var result = JObject . Parse ( content ) . ToObject < OpenIddictResponse > ( ) ;
2019-07-01 05:39:25 +02:00
Assert . NotEmpty ( result . AccessToken ) ;
Assert . Null ( result . Error ) ;
return result . AccessToken ;
}
private static async Task < string > RegisterClientCredentialsFlowAndGetAccessToken ( TestAccount user ,
string secret ,
ServerTester tester )
{
var id = Guid . NewGuid ( ) . ToString ( ) ;
var openIdClient = await user . RegisterOpenIdClient (
new OpenIddictApplicationDescriptor ( )
{
ClientId = id ,
DisplayName = id ,
2019-09-11 16:22:41 +09:00
Permissions = { OpenIddictConstants . Permissions . GrantTypes . ClientCredentials }
2019-07-01 05:39:25 +02:00
} , secret ) ;
var httpClient = tester . PayTester . HttpClient ;
var httpRequest = new HttpRequestMessage ( HttpMethod . Post ,
new Uri ( tester . PayTester . ServerUri , "/connect/token" ) )
{
Content = new FormUrlEncodedContent ( new List < KeyValuePair < string , string > > ( )
{
new KeyValuePair < string , string > ( "grant_type" ,
OpenIddictConstants . GrantTypes . ClientCredentials ) ,
new KeyValuePair < string , string > ( "client_id" , openIdClient . ClientId ) ,
2019-10-08 15:21:30 +09:00
new KeyValuePair < string , string > ( "client_secret" , secret ) ,
new KeyValuePair < string , string > ( "scope" , "server_management store_management" )
2019-07-01 05:39:25 +02:00
} )
} ;
var response = await httpClient . SendAsync ( httpRequest ) ;
Assert . True ( response . IsSuccessStatusCode ) ;
string content = await response . Content . ReadAsStringAsync ( ) ;
2019-10-05 20:44:12 +09:00
var result = JObject . Parse ( content ) . ToObject < OpenIddictResponse > ( ) ;
2019-07-01 05:39:25 +02:00
Assert . NotEmpty ( result . AccessToken ) ;
Assert . Null ( result . Error ) ;
return result . AccessToken ;
}
private static async Task < string > RegisterPasswordClientAndGetAccessToken ( TestAccount user , string secret ,
ServerTester tester )
{
var id = Guid . NewGuid ( ) . ToString ( ) ;
var openIdClient = await user . RegisterOpenIdClient (
new OpenIddictApplicationDescriptor ( )
{
ClientId = id ,
DisplayName = id ,
2019-09-11 16:22:41 +09:00
Permissions = { OpenIddictConstants . Permissions . GrantTypes . Password }
2019-07-01 05:39:25 +02:00
} , secret ) ;
var httpClient = tester . PayTester . HttpClient ;
var httpRequest = new HttpRequestMessage ( HttpMethod . Post ,
new Uri ( tester . PayTester . ServerUri , "/connect/token" ) )
{
Content = new FormUrlEncodedContent ( new List < KeyValuePair < string , string > > ( )
{
new KeyValuePair < string , string > ( "grant_type" , OpenIddictConstants . GrantTypes . Password ) ,
new KeyValuePair < string , string > ( "username" , user . RegisterDetails . Email ) ,
new KeyValuePair < string , string > ( "password" , user . RegisterDetails . Password ) ,
new KeyValuePair < string , string > ( "client_id" , openIdClient . ClientId ) ,
2019-10-08 15:21:30 +09:00
new KeyValuePair < string , string > ( "client_secret" , secret ) ,
new KeyValuePair < string , string > ( "scope" , "server_management store_management" )
2019-07-01 05:39:25 +02:00
} )
} ;
var response = await httpClient . SendAsync ( httpRequest ) ;
Assert . True ( response . IsSuccessStatusCode ) ;
string content = await response . Content . ReadAsStringAsync ( ) ;
2019-10-05 20:44:12 +09:00
var result = JObject . Parse ( content ) . ToObject < OpenIddictResponse > ( ) ;
2019-07-01 05:39:25 +02:00
Assert . NotEmpty ( result . AccessToken ) ;
Assert . Null ( result . Error ) ;
return result . AccessToken ;
}
2019-07-04 21:18:16 +09:00
async Task TestApiAgainstAccessToken ( string accessToken , ServerTester tester , TestAccount testAccount )
2019-07-01 05:39:25 +02:00
{
var resultUser =
await TestApiAgainstAccessToken < string > ( accessToken , "api/test/me/id" ,
tester . PayTester . HttpClient ) ;
Assert . Equal ( testAccount . UserId , resultUser ) ;
var secondUser = tester . NewAccount ( ) ;
secondUser . GrantAccess ( ) ;
var resultStores =
await TestApiAgainstAccessToken < StoreData [ ] > ( accessToken , "api/test/me/stores" ,
tester . PayTester . HttpClient ) ;
Assert . Contains ( resultStores ,
data = > data . Id . Equals ( testAccount . StoreId , StringComparison . InvariantCultureIgnoreCase ) ) ;
Assert . DoesNotContain ( resultStores ,
data = > data . Id . Equals ( secondUser . StoreId , StringComparison . InvariantCultureIgnoreCase ) ) ;
Assert . True ( await TestApiAgainstAccessToken < bool > ( accessToken ,
$"api/test/me/stores/{testAccount.StoreId}/can-edit" ,
tester . PayTester . HttpClient ) ) ;
2019-10-12 20:35:30 +09:00
Assert . True ( await TestApiAgainstAccessToken < bool > ( accessToken ,
2019-07-01 05:39:25 +02:00
$"api/test/me/is-admin" ,
tester . PayTester . HttpClient ) ) ;
await Assert . ThrowsAnyAsync < HttpRequestException > ( async ( ) = >
{
await TestApiAgainstAccessToken < bool > ( accessToken , $"api/test/me/stores/{secondUser.StoreId}/can-edit" ,
tester . PayTester . HttpClient ) ;
} ) ;
}
public async Task < T > TestApiAgainstAccessToken < T > ( string accessToken , string url , HttpClient client )
{
var httpRequest = new HttpRequestMessage ( HttpMethod . Get ,
new Uri ( client . BaseAddress , url ) ) ;
httpRequest . Headers . Authorization = new AuthenticationHeaderValue ( "Bearer" , accessToken ) ;
var result = await client . SendAsync ( httpRequest ) ;
result . EnsureSuccessStatusCode ( ) ;
var rawJson = await result . Content . ReadAsStringAsync ( ) ;
if ( typeof ( T ) . IsPrimitive | | typeof ( T ) = = typeof ( string ) )
{
return ( T ) Convert . ChangeType ( rawJson , typeof ( T ) ) ;
}
return JsonConvert . DeserializeObject < T > ( rawJson ) ;
}
}
}