Skip to content

Commit d229d71

Browse files
csharpfritzCopilot
andcommitted
fix: add configurable RedirectUri and remove unnecessary w_member_social scope
- Add RedirectUri field to LinkedInConfiguration for explicit OAuth redirect URL - Update OAuth endpoints to use configured RedirectUri when set, with auto-detect fallback - Show configured vs detected redirect_uri in debug-auth endpoint - Remove w_member_social scope (only read access needed, write may need extra approval) - Add RedirectUri to admin UI with helper text Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4a55a04 commit d229d71

File tree

3 files changed

+30
-7
lines changed

3 files changed

+30
-7
lines changed

src/TagzApp.Blazor.Client/Components/Admin/LinkedIn.Config.Ui.razor

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
<dd>
2020
<input name="ClientSecret" type="password" @bind="Model.ClientSecret" placeholder="LinkedIn App Client Secret" />
2121
</dd>
22+
<dt><label for="RedirectUri">OAuth Redirect URI (optional):</label></dt>
23+
<dd>
24+
<InputText name="RedirectUri" @bind-Value="Model.RedirectUri" placeholder="e.g. https://localhost:7106/api/linkedin/callback" />
25+
<small class="text-muted">Leave blank to auto-detect. Set explicitly if OAuth fails.</small>
26+
</dd>
2227
<dt>Access Token:</dt>
2328
<dd>
2429
<span class="form-control-plaintext text-muted">@ObfuscateToken(Model.AccessToken)</span>
@@ -73,6 +78,7 @@
7378
{
7479
ClientId = providerConfiguration.GetConfigurationByKey("ClientId"),
7580
ClientSecret = providerConfiguration.GetConfigurationByKey("ClientSecret"),
81+
RedirectUri = providerConfiguration.GetConfigurationByKey("RedirectUri"),
7682
AccessToken = providerConfiguration.GetConfigurationByKey("AccessToken"),
7783
RefreshToken = providerConfiguration.GetConfigurationByKey("RefreshToken"),
7884
TokenExpiresAt = providerConfiguration.GetConfigurationByKey("TokenExpiresAt"),
@@ -121,6 +127,7 @@
121127

122128
providerConfiguration.SetConfigurationByKey("ClientId", Model.ClientId);
123129
providerConfiguration.SetConfigurationByKey("ClientSecret", Model.ClientSecret);
130+
providerConfiguration.SetConfigurationByKey("RedirectUri", Model.RedirectUri);
124131
providerConfiguration.SetConfigurationByKey("PollingIntervalMinutes", Model.PollingIntervalMinutes.ToString());
125132
providerConfiguration.SetConfigurationByKey("DailyCallBudget", Model.DailyCallBudget.ToString());
126133
providerConfiguration.SetConfigurationByKey("Enabled", Model.Enabled.ToString());
@@ -143,6 +150,8 @@
143150

144151
public string ClientSecret { get; set; } = string.Empty;
145152

153+
public string RedirectUri { get; set; } = string.Empty;
154+
146155
public string AccessToken { get; set; } = string.Empty;
147156

148157
public string RefreshToken { get; set; } = string.Empty;

src/TagzApp.Blazor/Service_LinkedInOAuth.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public static class Service_LinkedInOAuth
1414
private const string LinkedInTokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken";
1515

1616
// Required scopes: r_member_social reads posts (needs Community Management API product)
17-
private static readonly string[] RequiredScopes = ["r_member_social", "w_member_social"];
17+
private static readonly string[] RequiredScopes = ["r_member_social"];
1818

1919
public static void MapLinkedInOAuthEndpoints(this WebApplication app)
2020
{
@@ -39,13 +39,18 @@ public static void MapLinkedInOAuthEndpoints(this WebApplication app)
3939
? request.Headers["X-Forwarded-Host"].ToString()
4040
: request.Host.ToString();
4141

42-
var redirectUri = $"{scheme}://{host}/api/linkedin/callback";
42+
var detectedRedirectUri = $"{scheme}://{host}/api/linkedin/callback";
43+
var configuredRedirectUri = linkedInConfig.RedirectUri;
44+
var usingConfigured = !string.IsNullOrEmpty(configuredRedirectUri);
45+
var redirectUri = usingConfigured ? configuredRedirectUri : detectedRedirectUri;
4346
var scope = string.Join(" ", RequiredScopes);
4447

4548
return Results.Json(new
4649
{
4750
clientId = linkedInConfig.ClientId,
4851
redirectUri,
52+
configuredRedirectUri,
53+
usingConfigured,
4954
scope,
5055
detectedScheme = scheme,
5156
detectedHost = host,
@@ -98,10 +103,12 @@ public static void MapLinkedInOAuthEndpoints(this WebApplication app)
98103
? request.Headers["X-Forwarded-Host"].ToString()
99104
: request.Host.ToString();
100105

101-
var redirectUri = $"{scheme}://{host}/api/linkedin/callback";
106+
var redirectUri = !string.IsNullOrEmpty(linkedInConfig.RedirectUri)
107+
? linkedInConfig.RedirectUri
108+
: $"{scheme}://{host}/api/linkedin/callback";
102109

103-
logger.LogInformation("LinkedIn OAuth authorize: Scheme={Scheme}, Host={Host}, RedirectUri={RedirectUri}, ClientId={ClientIdPrefix}...",
104-
scheme, host, redirectUri, linkedInConfig.ClientId[..Math.Min(4, linkedInConfig.ClientId.Length)]);
110+
logger.LogInformation("LinkedIn OAuth authorize: Scheme={Scheme}, Host={Host}, RedirectUri={RedirectUri}, UsingConfigured={UsingConfigured}, ClientId={ClientIdPrefix}...",
111+
scheme, host, redirectUri, !string.IsNullOrEmpty(linkedInConfig.RedirectUri), linkedInConfig.ClientId[..Math.Min(4, linkedInConfig.ClientId.Length)]);
105112

106113
// Generate state parameter for CSRF protection
107114
var state = Guid.NewGuid().ToString("N");
@@ -192,7 +199,9 @@ public static void MapLinkedInOAuthEndpoints(this WebApplication app)
192199
? request.Headers["X-Forwarded-Host"].ToString()
193200
: request.Host.ToString();
194201

195-
var redirectUri = $"{scheme}://{host}/api/linkedin/callback";
202+
var redirectUri = !string.IsNullOrEmpty(linkedInConfig.RedirectUri)
203+
? linkedInConfig.RedirectUri
204+
: $"{scheme}://{host}/api/linkedin/callback";
196205

197206
try
198207
{

src/TagzApp.Providers.LinkedIn/LinkedInConfiguration.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,23 @@ public class LinkedInConfiguration : IProviderConfiguration
1616

1717
public string ClientId { get; set; } = string.Empty;
1818
public string ClientSecret { get; set; } = string.Empty;
19+
public string RedirectUri { get; set; } = string.Empty;
1920
public string AccessToken { get; set; } = string.Empty;
2021
public string RefreshToken { get; set; } = string.Empty;
2122
public string TokenExpiresAt { get; set; } = string.Empty;
2223
public int PollingIntervalMinutes { get; set; } = 5;
2324
public int DailyCallBudget { get; set; } = 100;
2425

2526
[JsonIgnore]
26-
public string[] Keys => ["ClientId", "ClientSecret", "AccessToken", "RefreshToken", "TokenExpiresAt", "PollingIntervalMinutes", "DailyCallBudget"];
27+
public string[] Keys => ["ClientId", "ClientSecret", "RedirectUri", "AccessToken", "RefreshToken", "TokenExpiresAt", "PollingIntervalMinutes", "DailyCallBudget"];
2728

2829
public string GetConfigurationByKey(string key)
2930
{
3031
return key switch
3132
{
3233
"ClientId" => ClientId,
3334
"ClientSecret" => ClientSecret,
35+
"RedirectUri" => RedirectUri,
3436
"AccessToken" => AccessToken,
3537
"RefreshToken" => RefreshToken,
3638
"TokenExpiresAt" => TokenExpiresAt,
@@ -51,6 +53,9 @@ public void SetConfigurationByKey(string key, string value)
5153
case "ClientSecret":
5254
ClientSecret = value;
5355
break;
56+
case "RedirectUri":
57+
RedirectUri = value;
58+
break;
5459
case "AccessToken":
5560
AccessToken = value;
5661
break;

0 commit comments

Comments
 (0)