Я хотел бы сделать Identity-сервер и API в двух отдельных докерах для собственного приложения (клиентского мобильного).
Он работает на обратном прокси-сервере NGINX и Let's Encrypt.
Dockers
---------------------------
| Reverse Proxy |
| ----------------------- |
-------- | | ---------------- | |
| Mobile | ---------|-> | IdendityServer | | |
-------- | | | Port: 5000 | | |
| | | ---------------- | |
| | | | | |
| | | ---------------- | |
---------------|-> | API | | |
| | | Port: 5001 | | |
| | ---------------- | |
| ----------------------- |
| |
| ---------------- |
| | PostgreSQL | |
| | Port: 5432 | |
| ---------------- |
---------------------------
С моей текущей конфигурацией:
- Обратный прокси с Let's Encrypt хорошо работает с мобильного телефона
- API вызова без [Authority] хорошо работает с мобильного телефона
- Соединение Identity Server с гибридным потоком работает, и в списке претензий моего пользователя указан
Мои коды ниже.
IckerityServer Dockerfile
FROM microsoft/dotnet:2.0-sdk
COPY is4/* /app/
WORKDIR /app
ENV ASPNETCORE_URLS http://*:5000
EXPOSE 5000
ENTRYPOINT ["dotnet", "IdentityServer.dll"]
API Dockerfile
FROM microsoft/dotnet:2.0-sdk
COPY api/* /app/
WORKDIR /app
ENV ASPNETCORE_URLS http://*:5001
EXPOSE 5001
ENTRYPOINT ["dotnet", "ApiServer.dll"]
DockerCompose
version: '3'
services:
identityserver:
image: identityserver
build:
context: .
dockerfile: IdentityServer/Dockerfile
container_name: ids
restart: always
ports:
- 5000:5000
# expose:
# - "5000"
environment:
ASPNETCORE_ENVIRONMENT: Development
VIRTUAL_PORT: 5000
VIRTUAL_HOST: ids.mydomain.com
LETSENCRYPT_HOST: ids.mydomain.com
LETSENCRYPT_EMAIL: myuser@mydomain.com
IDENTITY_ISSUER: "https://ids.mydomain.com"
IDENTITY_REDIRECT: "com.mobiletest.nativeapp"
IDENTITY_CORS_ORIGINS: "https://ids.mydomain.com"
depends_on:
- db
apiserver:
image: apiserver
build:
context: .
dockerfile: ApiServer/Dockerfile
container_name: api
restart: always
ports:
- 5001:5001
# expose:
# - "5001"
environment:
ASPNETCORE_ENVIRONMENT: Development
VIRTUAL_PORT: 5001
VIRTUAL_HOST: api.mydomain.com
LETSENCRYPT_HOST: api.mydomain.com
LETSENCRYPT_EMAIL: myuser@mydomain.com
IDENTITY_AUTHORITY: "http://identityserver:5000"
CLIENT_CORS_ORIGINS: "com.mobiletest.nativeapp"
depends_on:
- identityserver
- db
links:
- identityserver
db:
image: postgresql:10
build:
context: .
dockerfile: PostgreSQL/Dockerfile
container_name: db
restart: always
ports:
- "5432:5432"
volumes:
- /www/database:/var/lib/postgresql/data
environment:
- PGDATA=/var/lib/postgresql/data/pgdata
networks:
default:
external:
name: nginx-proxy
Код запуска IdentityServer
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc();
// Configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer(opt =>
{
opt.IssuerUri = Configuration["IDENTITY_ISSUER"];
opt.PublicOrigin = Configuration["IDENTITY_ISSUER"];
})
.AddCorsPolicyService<InMemoryCorsPolicyService>() // Add the CORS service
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<ApplicationUser>();
services.AddAuthentication();
// preserve OIDC state in cache (solves problems with AAD and URL lenghts)
services.AddOidcStateDataFormatterCache("aad");
// add CORS policy for non-IdentityServer endpoints
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", policy =>
{
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
});
});
} // ConfigureServices()
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseIdentityServer();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
} // Configure()
Код конфигурации IdentityServer
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
{
ApiSecrets = { new Secret("secret".Sha256()) }
}
};
}
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "native.hybrid",
ClientName = "Native Client (Hybrid with PKCE)",
AllowedGrantTypes = GrantTypes.Hybrid,
RequirePkce = true,
RequireConsent = false,
//RequireClientSecret = false,
ClientSecrets = { new Secret("secret".Sha256()) },
RedirectUris = { Configuration["IDENTITY_REDIRECT"] + "://signin-oidc" },
PostLogoutRedirectUris = { Configuration["IDENTITY_REDIRECT"] + "://signout-callback-oidc" },
AllowedScopes = { "openid", "profile" },
AllowedCorsOrigins = { Configuration["IDENTITY_CORS_ORIGINS"] },
AllowOfflineAccess = true,
//AllowAccessTokensViaBrowser = true
RefreshTokenUsage = TokenUsage.ReUse
}
};
} // GetClients()
Api Config code
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
if (Configuration["CLIENT_CORS_ORIGINS"] == "")
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder
.AllowAnyMethod()
.AllowAnyOrigin()
.AllowAnyHeader());
});
}
else
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder
.AllowAnyHeader()
.AllowAnyMethod()
.WithOrigins(Configuration["CLIENT_CORS_ORIGINS"]));
});
}
services.AddAuthentication("Bearer");
services.AddAuthentication(options => //adds the authentication services to DI
{
//We are using a cookie as the primary means to authenticate a user (via “Cookies” as the DefaultScheme). We set the DefaultChallengeScheme to “oidc” because when we need the user to login, we will be using the OpenID Connect scheme.
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies") //add the handler that can process cookies
.AddOpenIdConnect("oidc", options => //configure the handler that perform the OpenID Connect protocol
{
options.SignInScheme = "Cookies"; //is used to issue a cookie using the cookie handler once the OpenID Connect protocol is complete
options.Authority = Configuration["IDENTITY_AUTHORITY"]; //indicates that we are trusting IdentityServer
options.RequireHttpsMetadata = false;
options.ClientId = "native.hybrid";
options.SaveTokens = true;
options.ClientSecret = "secret"; //used to persist the tokens from IdentityServer in the cookie
options.ResponseType = "code id_token";
});
services.AddMvc();
} // ConfigureServices()
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseCors("CorsPolicy");
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseMvc();
} // Configure()
Код контроллера API
[Route("api/[controller]")]
[EnableCors("CorsPolicy")]
[Authorize]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "testvalue1", "testvalue2" };
}
}
на Xamarin для клиента мобильного
var options = new OidcClientOptions
{
Authority = "https://ids4.syladebox.com",
ClientId = "native.hybrid",
ClientSecret = "secret",
//Scope = "openid profile api1 offline_access",
Scope = "openid profile offline_access",
ResponseMode = OidcClientOptions.AuthorizeResponseMode.Redirect,
RedirectUri = "com.mobiletest.nativeapp://signin-oidc",
PostLogoutRedirectUri = "com.mobiletest.nativeapp://signout-callback-oidc",
//Flow = OidcClientOptions.AuthenticationFlow.Hybrid,
//Policy = policy,
//Browser = new SFAuthenticationSessionBrowser()
// new in iOS 12
Browser = new ASWebAuthenticationSessionBrowser()
//Browser = new PlatformWebView()
};
_client = new OidcClient(options);
var result = await _client.LoginAsync(new LoginRequest());
if (result.IsError)
{
OutputText.Text = result.Error;
return;
}
if (result.AccessToken != null)
{
var client = new HttpClient();
client.SetBearerToken(result.AccessToken);
var response = await client.GetAsync("https://api.mydomain.com/api/values");
if (!response.IsSuccessStatusCode)
{
OutputText.Text = response.ReasonPhrase;
return;
}
var content = await response.Content.ReadAsStringAsync();
OutputText.Text = JArray.Parse(content).ToString();
}
Проблема не решена:
Проблема в том, что невозможно вызвать API от имени моего пользователя. Он возвращает либо либо «Нет авторизованного», либо «Bad gateway» после:
response = await client.GetAsync("https://api.mydomain.com/api/values");
Эти ошибки зависят от переменных среды в докерах IdentityServer и Api.
Мои текущие переменные среды:
IDENTITY_ISSUER: "https://ids.mydomain.com"
IDENTITY_REDIRECT: "com.mobiletest.nativeapp"
IDENTITY_CORS_ORIGINS: "https://ids.mydomain.com"
IDENTITY_AUTHORITY: "http://identityserver:5000"
CLIENT_CORS_ORIGINS: "com.mobiletest.nativeapp"
Вызов API (https://api.mydomain.com/api/values) возвращает «Bad gateway».
Я думаю, что IDENTITY_ISSUER, IDENTITY_REDIRECT являются исправлениями, потому что соединение с Identity Server успешно.
Проблема связана с другими переменными среды (IDENTITY_CORS_ORIGINS, IDENTITY_AUTHORITY и CLIENT_CORS_ORIGINS) или кодами Identity Server / API?
Обновление 26 января:
Чтобы убедиться, что моя программа API работает, я переделал программу API до самого простого:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Authority = Configuration["IDENTITY_AUTHORITY"];
options.ApiName = "api";
//options.ApiSecret = "secret";
});
// Add CORS policy for non-IdentityServer endpoints
services.AddCors(options =>
{
options.AddPolicy("api", policy =>
{
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
});
});
} // ConfigureServices()
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseCors("api");
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseMvc();
} // Configure()
с контроллером API:
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = IdentityServerAuthenticationDefaults.AuthenticationScheme)]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "testvalue1", "testvalue2" };
}
}
и я использую в своем первом тесте другой сервер идентификации:
demo.identityserver.io
Для этого теста я делаю следующую конфигурацию:
apiserver:
...
ports:
- 5001:80
environment:
...
IDENTITY_AUTHORITY: "https://demo.identityserver.io"
#CLIENT_CORS_ORIGINS (omitted in the code)
Мои OidcClientOptions в коде клиента:
var options = new OidcClientOptions
{
Authority = "https://demo.identityserver.io",
ClientId = "native.hybrid",
Scope = "openid profile email api offline_access",
ResponseMode = OidcClientOptions.AuthorizeResponseMode.Redirect,
RedirectUri = "com.mobiletest.nativeapp://callback",
PostLogoutRedirectUri = "com.mobiletest.nativeapp://callback",
Browser = new ASWebAuthenticationSessionBrowser()
};
Функция входа в систему - в моей первой теме.
- Соединение сервера идентификации с гибридным потоком работает
- Вызов API успешен!
Поскольку demo.identityserver.io является демонстрационным сервером идентификации, я сомневаюсь, что он работает как производственный вариант, тогда я протестировал другой сервер идентификации (Okta) с той же программой API:
dev-xxxxxx.okta.com
Для этого теста я делаю следующую конфигурацию:
apiserver:
...
ports:
- 5001:80
environment:
...
IDENTITY_AUTHORITY: "https://dev-xxxxxx.okta.com"
#CLIENT_CORS_ORIGINS (omitted in the code)
Мои OidcClientOptions в коде клиента:
var options = new OidcClientOptions
{
Authority = "https://dev-xxxx.okta.com",
ClientId = "xxxxxxxxxxxxxxxxxxx", // ClientId is hidden in this topic
Scope = "openid profile email offline_access",
ResponseMode = OidcClientOptions.AuthorizeResponseMode.Redirect,
RedirectUri = "com.okta.dev-xxxxxx:/callback",
PostLogoutRedirectUri = "com.okta.dev-xxxxxx:/callback",
Browser = new ASWebAuthenticationSessionBrowser()
};
- Соединение сервера идентификации с гибридным потоком работает
- Вызов API не работает, он возвращает сообщение «Unauthorized».
С помощью двух тестов я не могу определить, хорошо ли работает моя программа API.
Не могли бы вы помочь мне? Большое спасибо!