I am encountering difficulties with my Duende Identity Server implementation. The server successfully provides tokens to a WPF Desktop client, but I'm facing issues when trying to obtain an access token from the server using a mobile app.
I decided to use Duende Identity Server because the old IdentityServer4 is no longer supported. However, Duende Identity is still new and I rarely find any support on the Internet.
My project consists of:
A Duende Identity server that is responsible for giving tokens to native apps like: Desktop and Mobile.
A Protected API
A WPF Desktop client
A Mobile App client
This is my code:
Identity Server
Program.cs:
using AuthenticationServer;
using AuthenticationServer.Domain.Models;
using AuthenticationServer.Mapping;
using AuthenticationServer.Persistence;
using AuthenticationServer.Persistence.Seeders;
using Duende.IdentityServer.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
#region Configure services
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll",
builder =>
{
builder
.WithOrigins("http://localhost:3000", "https://authenticationserver2023.azurewebsites.net")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
builder.Services.AddControllers();
builder.Services.AddControllersWithViews();
builder.Services.AddAutoMapper(typeof(ModelToViewModelProfile));
builder.Services.AddDbContext(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("CloudConnection"));
});
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
{
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = false;
})
.AddEntityFrameworkStores()
.AddDefaultTokenProviders();
builder.Services
.AddIdentityServer(options =>
{
options.EmitStaticAudienceClaim = true;
})
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlServer(builder.Configuration.GetConnectionString("CloudConnection"),
b => b.MigrationsAssembly("AuthenticationServer"));
})
.AddConfigurationStoreCache()
.AddProfileService()
.AddAspNetIdentity();
builder.Services.AddLocalApiAuthentication();
builder.Services.AddSingleton((container) =>
{
var logger = container.GetRequiredService<ILogger>();
return new DefaultCorsPolicyService(logger)
{
AllowAll = true
};
});
builder.Services.AddScoped<IProfileService, ProfileService>();
builder.Services.AddScoped();
#endregion
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
using var identityServerSeeder = scope.ServiceProvider.GetService();
identityServerSeeder?.SeedData();
}
if (args.Length == 1 && args[0].ToLower() == "seeddata")
{
await SeedUsersRoles.SeedUsersAndRolesAsync(app);
}
app.UseCors("AllowAll");
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseIdentityServer();
app.UseStaticFiles();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapDefaultControllerRoute();
});
app.Run();
Config.cs
<pre>using Duende.IdentityServer;
using Duende.IdentityServer.Models;
namespace AuthenticationServer;
public static class Config
{
public static IEnumerable IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope("native-client-scope"),
new ApiScope(IdentityServerConstants.LocalApi.ScopeName),
};
public static IEnumerable<Client> Clients =>
new List<Client>
{
new Client {
ClientId = "native-client",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
RedirectUris = { "https://authenticationserver2023.azurewebsites.net/account/login" },
PostLogoutRedirectUris = { },
AllowedCorsOrigins = { "http://localhost", "https://authenticationserver2023.azurewebsites.net" },
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
"native-client-scope",
IdentityServerConstants.LocalApi.ScopeName,
IdentityServerConstants.StandardScopes.Profile
},
AllowAccessTokensViaBrowser = true,
RequireConsent = false,
AccessTokenLifetime = 8*3600
},
new Client {
ClientId = "user-management-app",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
RedirectUris = { "https://authenticationserver2023.azurewebsites.net/account/login" },
PostLogoutRedirectUris = { },
AllowedCorsOrigins = { "https://authenticationserver2023.azurewebsites.net" },
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.LocalApi.ScopeName,
IdentityServerConstants.StandardScopes.Profile
},
AllowAccessTokensViaBrowser = true,
RequireConsent = false,
},
};
}
AccountController.cs
<pre>using AuthenticationServer.Domain.Models;
using AuthenticationServer.ViewModels;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace AuthenticationServer.Controllers;
public class AccountController : Controller
{
private readonly SignInManager _signInManager;
public AccountController(SignInManager<ApplicationUser> signInManager)
{
_signInManager = signInManager;
}
[HttpGet]
public IActionResult Login()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Login(LoginInputModel model, [FromQuery] string returnUrl)
{
var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, false, false);
if (result.Succeeded)
{
return Redirect(returnUrl);
}
return View(true);
}
Mobile:
<pre>import 'package:flutter/material.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:http/http.dart' as http;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@OverRide
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter AppAuth Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@OverRide
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
final FlutterAppAuth _appAuth = FlutterAppAuth();
String? _accessToken;
String? _idToken;
String? _userInfo;
bool _isBusy = false;
@OverRide
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter AppAuth Example'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _isBusy ? null : _signIn,
child: const Text('Sign In'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _accessToken != null ? _callApi : null,
child: const Text('Call API'),
),
const SizedBox(height: 16),
if (_accessToken != null) Text('Access Token: $_accessToken'),
const SizedBox(height: 8),
if (_idToken != null) Text('ID Token: $_idToken'),
const SizedBox(height: 8),
if (_userInfo != null) Text('User Info: $_userInfo'),
const SizedBox(height: 8),
if (_isBusy) CircularProgressIndicator(),
],
),
),
);
}
Future _signIn() async {
try {
setState(() {
_isBusy = true;
});
final AuthorizationTokenResponse? result =
await _appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
'native-client',
'https://authenticationserver2023.azurewebsites.net/account/login',
serviceConfiguration: const AuthorizationServiceConfiguration(
tokenEndpoint:
'https://authenticationserver2023.azurewebsites.net/connect/token',
authorizationEndpoint:
'https://authenticationserver2023.azurewebsites.net/connect/authorize',
),
scopes: ['openid', 'profile', 'native-client-scope'],
promptValues: ['login'],
issuer: 'https://authenticationserver2023.azurewebsites.net',
),
);
if (result != null) {
_processAuthTokenResponse(result);
await _callApi();
}
} catch (e) {
print('Error during sign in: $e');
} finally {
setState(() {
_isBusy = false;
});
}
}
Future _callApi() async {
try {
final http.Response httpResponse = await http.get(
Uri.parse('https://protectedapi2023.azurewebsites.net/WeatherForecast'),
headers: <String, String>{'Authorization': 'Bearer $_accessToken'},
);
setState(() {
_userInfo =
httpResponse.statusCode == 200 ? httpResponse.body : 'API Error';
});
} catch (e) {
print('Error calling API: $e');
}
}
void _processAuthTokenResponse(AuthorizationTokenResponse response) {
setState(() {
_accessToken = response.accessToken;
_idToken = response.idToken;
});
}
}
What I have tried:
When using Mobile app to access the account/login endpoint and type in the correct username and password, the returnUrl parameter is happened to be null and then I got error 500