Config Duende Identity Server cho Mobile App

Mong mọi người trợ giúp. Em xin cảm ơn mn nhiều.
Hiện em đang học về IdentityServer và đang làm 1 bài tập nhỏ liên quan đến nó.
Project của em gồm 1 server, 1 mobile app, và 1 protected API. Khi Mobile app nhập đúng tên đăng nhập và mật khẩu thì Identity Server sẽ gửi về authorization code cho Mobile app. Mobile app sẽ dùng code đó gửi request tới tokenendpoint để đổi lấy accesstoken rồi truy cập vào Protected API.

Hiện tại em đang bị kẹt ở bước callback về app để lấy authorization code về.

Đây là code phía server của em:

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("localhost", "http://localhost:3000", "https://authenticationserver2023.azurewebsites.net")
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials();
        });
});

builder.Services.AddControllers();
builder.Services.AddControllersWithViews();

builder.Services.AddAutoMapper(typeof(ModelToViewModelProfile));

builder.Services.AddDbContext<ApplicationDbContext>(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<ApplicationDbContext>()
    .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<ProfileService>()
    .AddAspNetIdentity<ApplicationUser>();

builder.Services.AddLocalApiAuthentication();

builder.Services.AddSingleton<ICorsPolicyService>((container) =>
{
    var logger = container.GetRequiredService<ILogger<DefaultCorsPolicyService>>();
    return new DefaultCorsPolicyService(logger)
    {
        AllowAll = true
    };
});
builder.Services.AddScoped<IProfileService, ProfileService>();
builder.Services.AddScoped<IdentityServerConfigurationSeeder>();
#endregion

var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
    using var identityServerSeeder = scope.ServiceProvider.GetService<IdentityServerConfigurationSeeder>();
    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

using Duende.IdentityServer;
using Duende.IdentityServer.Models;

namespace AuthenticationServer;

public static class Config
{
    public static IEnumerable<IdentityResource> 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://localhost:7124/account/login" },
                    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
                }
        };
}

AccountController.cs

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<ApplicationUser> _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);
    }
}

Ngoài ra còn một số file model, viewmodel, trang login (project MVC)
Mobile App:

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<MyHomePage> {
  final FlutterAppAuth _appAuth = FlutterAppAuth();
  String? _accessToken;
  String? _userInfo;
  bool _isBusy = false;
  final response_type = 'code';
  final clientId = 'native-client';
  final redirectUri =
      'https://authenticationserver2023.azurewebsites.net/account/login';
  final scopes = ["openid", "profile", "native-client-scope"];

  @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 (_userInfo != null) Text('User Info: $_userInfo'),
            const SizedBox(height: 8),
            if (_isBusy) CircularProgressIndicator(),
          ],
        ),
      ),
    );
  }

  Future<void> _signIn() async {
    try {
      setState(() {
        _isBusy = true;
      });

      final AuthorizationTokenResponse? result =
          await _appAuth.authorizeAndExchangeCode(
        AuthorizationTokenRequest(
          clientId,
          redirectUri,
          serviceConfiguration: AuthorizationServiceConfiguration(
            tokenEndpoint:
                'https://authenticationserver2023.azurewebsites.net/connect/token',
            authorizationEndpoint:
                'https://authenticationserver2023.azurewebsites.net/connect/authorize'
          ),
          scopes: scopes,
          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<void> _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;
    });
  }
}

Tình trạng là cùng một server như trên, desktop app khi truy cập vô endpoint login thì có thể nhận về được token trong khi Mobile app lại dính lỗi Error 500 do returnUrl bị null nên redirect failed ạ.

83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?