ILINEService

public interface ILINEService
{
    string GenerateAuthUrls(string state, string redirectUrl);

    /// <summary>
    /// 取得Line Access Token(Line登入資料)
    /// <https://developers.line.biz/en/docs/line-login/web/integrate-line-login/#spy-getting-an-access-token>
    /// </summary>
    /// <param name="code"> 回傳的驗證碼 </param>
    /// <param name="redirectUrl"> LINE 回傳的接收網址 </param>
    /// <returns> LineLoginToken </returns>
    Task<LINELoginResource?> ChallengeToken(string code, string redirectUrl);

    /// <summary>
    /// 透過 accessToken 取得用戶基本資料
    /// </summary>
    /// <param name="accessToken"> Access Token </param>
    /// <returns> LINEProfile資料 </returns>
    Task<LINEUserProfile?> GetLINEProfile(string accessToken);

    /// <summary>
    /// 透過 idToken取得用戶資料資料
    /// </summary>
    /// <param name="idToken">id Token</param>
    /// <returns>LINEProfile資料</returns>
    Task<LINEUserProfile?> GetLINEProfileWithEmail(string idToken);

    /// <summary>
    /// 透過LINE發送訊息給使用者
    /// </summary>
    /// <param name="clientId"> clientId </param>
    /// <param name="message"> 訊息 </param>
    Task PushMessage(string clientId, List<string> message);
}

LINEService

public class LINEService(IHttpClientFactory clientFactory, IOptions<LINESettings> options) : ILINEService
{
    private readonly string _channelId = options.Value.ChannelId;
    private readonly string _channelSecret = options.Value.ChannelSecret;
    private readonly string _channelAccessToken = string.Empty;
    private readonly HttpClient _client = clientFactory.CreateClient();
    public const string AuthorizationEndpoint = "<https://access.line.me/oauth2/v2.1/authorize>";
    public const string TokenEndpoint = "<https://api.line.me/oauth2/v2.1/token>";
    public const string UserInformationEndpoint = "<https://api.line.me/v2/profile>";
    public const string UserEmailsEndpoint = "<https://api.line.me/oauth2/v2.1/verify>";
    public const string PushMessageEndpoint = "<https://api.line.me/v2/bot/message/push>";

    public string GenerateAuthUrls(string state, string redirectUrl)
    {
        var query = HttpUtility.ParseQueryString(string.Empty);
        query["scope"] = "openid profile email";
        query["response_type"] = "code";
        query["state"] = state;
        query["redirect_uri"] = redirectUrl;
        query["client_id"] = _channelId;

        return $"{AuthorizationEndpoint}?{query}";
    }

    public async Task<LINELoginResource?> ChallengeToken(string code, string redirectUrl)
    {
        Dictionary<string, string> formData = new()
        {
            { "grant_type", "authorization_code" },
            { "code", code },
            { "redirect_uri", redirectUrl },
            { "client_id", _channelId },
            { "client_secret", _channelSecret }
        };

        var response = await _client.PostAsync(TokenEndpoint, new FormUrlEncodedContent(formData));

        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<LINELoginResource>();
    }

    public async Task<LINEUserProfile?> GetLINEProfile(string accessToken)
    {
        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

        var response = await _client.GetAsync(UserInformationEndpoint);

        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<LINEUserProfile>();
    }

    public async Task<LINEUserProfile?> GetLINEProfileWithEmail(string idToken)
    {
        Dictionary<string, string> formData = new()
        {
            { "id_token", idToken },
            { "client_id", _channelId },
        };

        var response = await _client.PostAsync(UserEmailsEndpoint, new FormUrlEncodedContent(formData));
        response.EnsureSuccessStatusCode();

        using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync());

        return new LINEUserProfile()
        {
            UserId = payload.RootElement.GetString("sub")!,
            DisplayName = payload.RootElement.GetString("name")!,
            Email = payload.RootElement.GetString("email") ?? string.Empty,
        };
    }

    public async Task PushMessage(string clientId, List<string> message)
    {
        _client.DefaultRequestHeaders.Add("authorization", $"Bearer {_channelAccessToken}");

        var postMessage = new LINEMessage
        {
            To = clientId,
            Messages = message.Select(x => new Message { Text = x, Type = "text" }).ToList()
        };

        var json = JsonSerializer.Serialize(postMessage);
        HttpContent contentPost = new StringContent(json, Encoding.UTF8, "application/json");

        await _client.PostAsync(PushMessageEndpoint, contentPost);
    }
}

LINELoginResource

public class LINELoginResource
{
    /// <summary>
    /// Access token. 有效期為30天
    /// </summary>
    [JsonPropertyName("access_token")]
    public string AccessToken { get; set; } = null!;

    /// <summary>
    /// 訪問到期時間,以秒為單位
    /// </summary>
    [JsonPropertyName("expires_in")]
    public int ExpiresIn { get; set; }

    /// <summary>
    /// Token
    /// </summary>
    [JsonPropertyName("id_token")]
    public string IdToken { get; set; } = null!;

    /// <summary>
    /// 用於獲取新Token。有效期為90天。
    /// </summary>
    [JsonPropertyName("refresh_token")]
    public string RefreshToken { get; set; } = null!;

    /// <summary>
    /// 用戶授予的權限
    /// </summary>
    [JsonPropertyName("scope")]
    public string Scope { get; set; } = null!;

    /// <summary>
    /// Bearer
    /// </summary>
    [JsonPropertyName("token_type")]
    public string TokenType { get; set; } = null!;
}

LINEMessage

public class LINEMessage
{
    [JsonPropertyName("to")]
    public string To { get; set; } = string.Empty;

    [JsonPropertyName("messages")]
    public List<Message> Messages { get; set; } = [];
}

public class Message
{
    [JsonPropertyName("type")]
    public string Type { get; set; } = string.Empty;

    [JsonPropertyName("text")]
    public string Text { get; set; } = string.Empty;
}

LINEUserProfile

public class LINEUserProfile
{
    public string UserId { get; set; } = null!;
    public string DisplayName { get; set; } = string.Empty;
    public string StatusMessage { get; set; } = string.Empty;
    public string PictureUrl { get; set; } = string.Empty;

    /// <summary>
    /// 需透過idToken才能取得Email
    /// </summary>
    public string Email { get; set; } = string.Empty;
}

LINESettings

public class LINESettings
{
    /// <summary>
    /// LINE Login ChannelId
    /// </summary>
    public string ChannelId { get; set; } = null!;

    /// <summary>
    /// LINE Login ChannelSecret
    /// </summary>
    public string ChannelSecret { get; set; } = null!;

    /// <summary>
    /// LINE Message Api ChannelAccessToken
    /// </summary>
    public string ChannelAccessToken { get; set; } = string.Empty;
}

Program

builder.Services.Configure<LINESettings>(builder.Configuration.GetSection("Authentication:LINE"));
services.AddScoped<ILINEService, LINEService>();