-
-
Notifications
You must be signed in to change notification settings - Fork 526
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generating a token on behalf of a user #1241
Comments
OpenIddict only generates tokens in the context of an OIDC request. For your scenario, you'll likely directly want to create the tokens using IdentityModel, the MSFT stack OpenIddict uses to generate tokens. |
Thank you. I dug through the code and found out how to fill my needs here. If anyone else needs to do this in the future, here's what I did. Ideally nobody should ever be using this, but unfortunately IFTTT requires it for their automated tests. Get a reference to var options = _oidcOptions.CurrentValue;
var descriptor = new SecurityTokenDescriptor
{
Claims = new Dictionary<string, object>
{
{ "sub", "your user id" },
{ "scope", "your scopes" },
},
EncryptingCredentials = options.DisableAccessTokenEncryption
? null
: options.EncryptionCredentials.First(),
Expires = null, // recommended to set this
IssuedAt = DateTime.UtcNow,
Issuer = "https://contoso.com/", // the URL your auth server is hosted on, with trailing slash
SigningCredentials = options.SigningCredentials.First(),
TokenType = OpenIddictConstants.JsonWebTokenTypes.AccessToken,
};
var accessToken = options.JsonWebTokenHandler.CreateToken(descriptor); |
@Rohansi thanks for this, it saves me a lot of time to dig into the source code |
@Rohansi Thank you! This saved my day! |
Thank you for this input! We've had troubles implementing our maui essentials web authenticator backend. There were basically 2 ways we could implement this: 1st approach: Redirect to /exchange/tokenIn our [HttpPost]
[Route("login")]
public async Task<IHttpActionResult> Login([FromBody] LoginRequest request)
{
// Prepare the request to OpenIddict's token endpoint (/connect/token)
using (var client = new HttpClient())
{
var tokenEndpoint = new Uri(Request.RequestUri,"/connect/token").ToString();
var tokenRequest = new Dictionary<string, string>
{
{ "grant_type", "password" },
{ "username", request.Username },
{ "password", request.Password },
{ "client_id", "the-client-id" },
{ "client_secret", "the-client-secret" },
{ "scope", "offline_access" },
{"tenancyName",request.TenancyName}
};
var content = new FormUrlEncodedContent(tokenRequest);
// Send request to the token endpoint
var response = await client.PostAsync(tokenEndpoint, content);
if (!response.IsSuccessStatusCode)
{
return BadRequest("Invalid login attempt.");
}
// Parse the response to extract access_token and refresh_token
var responseString = await response.Content.ReadAsStringAsync();
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(responseString);
// Build the query parameters to return back to the MAUI app
var qs = new Dictionary<string, string>
{
{ "access_token", tokenResponse.AccessToken },
{ "refresh_token", tokenResponse.RefreshToken },
{ "expires_in", tokenResponse.ExpiresIn.ToString() },
{ "token_type", tokenResponse.TokenType }
};
// Build the result URL (redirect back to MAUI app)
var url = callbackScheme + "://#" + string.Join(
"&",
qs.Where(kvp => !string.IsNullOrEmpty(kvp.Value))
.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}"));
// Redirect to the final URL
return Redirect(url);
}
} This has, however, the downside that we
2nd approach: Create the tokens on-the-flyThis is the approach you took and I honestly like it better: [HttpPost]
[Route("login2")]
public async Task<IHttpActionResult> Login2([FromBody] LoginRequest request)
{
// Step 1: Authenticate the user (replace with actual user authentication logic)
// Note: We are using Aspnetboilerplate - thus, the loginmanager
var loginResult = await _loginManager.LoginAsync(request.Username, request.Password, request.TenancyName);
if (loginResult.Result != AbpLoginResultType.Success)
{
return Unauthorized();
}
// Step 2: Create the claims principal for the authenticated user
var identity = new ClaimsIdentity(loginResult.Identity.Claims,
authenticationType: OpenIddictServerOwinDefaults.AuthenticationType,
nameType: AbpClaimTypes.UserName,
roleType: OpenIddictConstants.Claims.Role);
var claimsPrincipal = new ClaimsPrincipal(identity);
claimsPrincipal.SetDestinations(static claim => OpenIddictUtil.GetDestinations(claim));
var destins = claimsPrincipal.GetDestinations()
.Where(k => k.Value.Contains(OpenIddictConstants.Destinations.AccessToken))
.Select(k => k.Key).ToHashSet();
var user = loginResult.User;
OpenIddictServerOptions options = _options.CurrentValue;
var descriptor = new SecurityTokenDescriptor
{
Claims = new Dictionary<string, object>
{
{ "sub", user.Id },
// { "scope", "your scopes" },
}.Concat(claimsPrincipal.Claims.Where(c => destins.Contains(c.Type)).Select(c => new KeyValuePair<string, object>(c.Type,c.Value)))
.GroupBy(kvp => kvp.Key).Select(g => g.First())
.ToDictionary(k => k.Key, k => k.Value),
EncryptingCredentials = options.DisableAccessTokenEncryption
? null
: options.EncryptionCredentials.First(),
Expires = DateTime.UtcNow.Add(options.AccessTokenLifetime??TimeSpan.FromHours(1)), // recommended to set this
IssuedAt = DateTime.UtcNow,
Issuer = options.Issuer?.ToString(), // the URL your auth server is hosted on, with trailing slash
SigningCredentials = options.SigningCredentials.First(),
TokenType = OpenIddictConstants.JsonWebTokenTypes.AccessToken,
};
var accessToken = options.JsonWebTokenHandler.CreateToken(descriptor);
var descriptor2 = new SecurityTokenDescriptor
{
Claims = new Dictionary<string, object>
{
{ "sub", user.Id },
// { "scope", "your scopes" },
},
EncryptingCredentials = options.DisableAccessTokenEncryption
? null
: options.EncryptionCredentials.First(),
Expires = DateTime.UtcNow.Add(options.RefreshTokenLifetime??TimeSpan.FromDays(14)), // recommended to set this
IssuedAt = DateTime.UtcNow,
Issuer = options.Issuer?.ToString(), // the URL your auth server is hosted on, with trailing slash
SigningCredentials = options.SigningCredentials.First(),
TokenType = OpenIddictConstants.JsonWebTokenTypes.Private.RefreshToken
};
var refreshToken = options.JsonWebTokenHandler.CreateToken(descriptor2);
// Step 6: Redirect to the MAUI app with the tokens
var tokenResponse = new Dictionary<string, string>
{
{ "access_token", accessToken },
{ "refresh_token", refreshToken ?? string.Empty },
{ "expires_in", ((int)TimeSpan.FromHours(1).TotalSeconds).ToString() },
{ "token_type", "Bearer" }
};
// Step 7: Minimalistic validation of the client and redirecturi
var appMgr = _provider.GetRequiredService<IOpenIddictApplicationManager>();
var app = (OpenIddictApplication)await appMgr.FindByClientIdAsync(request.ClientId);
var validRedirectUri = app?.RedirectUris?.Contains(request.RedirectUri) ?? false;
if (!validRedirectUri)
{
Logger.Warn($"requested redirectUri {request.RedirectUri} is not contained in the apps RedirectUris: {app?.RedirectUris}");
return Unauthorized();
}
var fragment = $"#{string.Join("&", tokenResponse.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}"))}";
var callbackUrl = new Uri(new Uri(request.RedirectUri),fragment);
var logUrl = new Uri(new Uri(request.RedirectUri),$"#{string.Join("&", tokenResponse.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(new string('*', kvp.Value.Length))}"))}");
Logger.Debug($"Redirecting to {logUrl}");
return Redirect(callbackUrl);
} However, I am not sure how safe this is. :-| Any thoughts? Note: Our server is installed on multiple sites (customers) with its own FQDN. But the app is installed via App Store/Google Play/Microsoft Store. So we need a generic login page that can be controlled from the server and that - upon successful authentication - redirects to the uri the app requires for the Maui Essentials webauthenticator. |
Both options are absolutely terrible (and the reason why I don't want to encourage users to "generate their own tokens" outside a standard flow). The correct approach is to use a proper OIDC client library in your MAUI app - e.g OpenIddict: https://github.com/openiddict/openiddict-core/tree/dev/sandbox/OpenIddict.Sandbox.Maui.Client 🤣 - and use the OIDC code flow to communicate interactively with your OIDC server in a completely standard way. Everything else is just a hack, IMHO. |
Well, could be, but having had a look at your sample, I have the following problems:
In our app, the first thing you will see is a screen asking for the URL of the server you'd like to connect to (which then is the issuer)
So given this, how can we actually use the client library? :-| And please don't be humble - you're the professional here 😅 |
Well, "dynamic" clients are not officially supported by the OpenIddict (at least not yet), but you have two viable approaches to solve that:
class MyClientService(IServiceProvider provider) : OpenIddictClientService(provider)
{
private readonly IServiceProvider _provider = provider ?? throw new ArgumentNullException(nameof(provider));
private readonly ConcurrentDictionary<string, OpenIddictClientRegistration> _registrations = new();
public override ValueTask<OpenIddictClientRegistration> GetClientRegistrationByIdAsync(
string identifier, CancellationToken cancellationToken = default)
{
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
// If a static client registration using the specified identifier was added to the client options, always prefer it.
var registration = options.CurrentValue.Registrations.Find(registration => string.Equals(
registration.RegistrationId, identifier, StringComparison.Ordinal));
if (registration is not null)
{
return ValueTask.FromResult(registration);
}
if (_registrations.TryGetValue(identifier, out registration))
{
return ValueTask.FromResult(registration);
}
throw new InvalidOperationException();
}
public override ValueTask<OpenIddictClientRegistration> GetClientRegistrationByIssuerAsync(
Uri uri, CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
{
return ValueTask.FromCanceled<OpenIddictClientRegistration>(cancellationToken);
}
// If a static client registration using the specified issuer was added to the client options, always prefer it.
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
if (options.CurrentValue.Registrations.Find(registration => registration.Issuer == uri)
is OpenIddictClientRegistration registration)
{
return ValueTask.FromResult(registration);
}
// Generate a stable client registration identifier based on the issuer URI.
var identifier = Base64Url.EncodeToString(SHA256.HashData(Encoding.UTF8.GetBytes(uri.AbsoluteUri)));
return ValueTask.FromResult(_registrations.GetOrAdd(identifier, _ =>
{
var registration = new OpenIddictClientRegistration
{
RegistrationId = identifier,
Issuer = uri,
ProviderName = "User-defined authorization server",
ConfigurationEndpoint = new Uri(uri, ".well-known/openid-configuration"),
ClientId = "console",
PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative),
RedirectUri = new Uri("callback/login/local", UriKind.Relative),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }
};
// Note: unlike a static registration - for which the configuration manager is instantiated for you -
// a dynamic registration requires attaching it manually. Caching the instance is strongly recommended.
registration.ConfigurationManager = new ConfigurationManager<OpenIddictConfiguration>(
registration.ConfigurationEndpoint.AbsoluteUri, new OpenIddictClientRetriever(this, registration))
{
AutomaticRefreshInterval = ConfigurationManager<OpenIddictConfiguration>.DefaultAutomaticRefreshInterval,
RefreshInterval = ConfigurationManager<OpenIddictConfiguration>.DefaultRefreshInterval
};
return registration;
}));
}
} services.Replace(ServiceDescriptor.Singleton<OpenIddictClientService, MyClientService>()); // Ask OpenIddict to initiate the authentication flow (typically, by starting the system browser).
var result = await _service.ChallengeInteractivelyAsync(new()
{
CancellationToken = stoppingToken,
Issuer = new Uri("https://yourprovider.com/", UriKind.Absolute)
});
AnsiConsole.MarkupLine("[cyan]Waiting for the user to approve the authorization demand.[/]");
// Wait for the user to complete the authorization process and authenticate the callback request,
// which allows resolving all the claims contained in the merged principal created by OpenIddict.
var response = await _service.AuthenticateInteractivelyAsync(new()
{
CancellationToken = stoppingToken,
Nonce = result.Nonce
}); Caution It seems obvious, but using this approach means you're basically trusting any server configured by the user: it may be fine for a desktop or mobile app but may not be what you want for a web-app where you only want users to use pre-configured/trusted authorization servers: I know it's what you want, but I'm sure less careful users will end up blindly copying this snippet without necessarily realizing it 😄 In both cases, you'll need to configure the redirection and post-logout-redirection endpoints manually, since the addresses cannot be inferred from the configured registrations in that case: options.SetRedirectionEndpointUris("callback/login/local")
.SetPostLogoutRedirectionEndpointUris("callback/logout/local"); There are also other OAuth 2.0/OIDC libraries that offer different approaches (like |
I am really greatful for all your insights! Here is what I am planning to do: Participants:
Sequence of Events:
Using your first recommended approach (Microsoft.Extensions.Options) ist not my favorite since dont have the feeling that I can see all the moving parts. I would not even know where to start. The second approach seems more reasonable to me since I could inject my logic here: class MyClientService(IServiceProvider provider) : OpenIddictClientService(provider)
{
private readonly IServiceProvider _provider = provider ?? throw new ArgumentNullException(nameof(provider));
private readonly ConcurrentDictionary<string, OpenIddictClientRegistration> _registrations = new();
public override ValueTask<OpenIddictClientRegistration> GetClientRegistrationByIdAsync(
string identifier, CancellationToken cancellationToken = default)
{
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
// If a static client registration using the specified identifier was added to the client options, always prefer it.
var registration = options.CurrentValue.Registrations.Find(registration => string.Equals(
registration.RegistrationId, identifier, StringComparison.Ordinal));
if (registration is not null)
{
return ValueTask.FromResult(registration);
}
if (_registrations.TryGetValue(identifier, out registration))
{
return ValueTask.FromResult(registration);
}
throw new InvalidOperationException();
}
public override ValueTask<OpenIddictClientRegistration> GetClientRegistrationByIssuerAsync(
Uri uri, CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
{
return ValueTask.FromCanceled<OpenIddictClientRegistration>(cancellationToken);
}
// If a static client registration using the specified issuer was added to the client options, always prefer it.
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
if (options.CurrentValue.Registrations.Find(registration => registration.Issuer == uri)
is OpenIddictClientRegistration registration)
{
return ValueTask.FromResult(registration);
}
// Generate a stable client registration identifier based on the issuer URI.
var identifier = Base64Url.EncodeToString(SHA256.HashData(Encoding.UTF8.GetBytes(uri.AbsoluteUri)));
+ // call the server and start the process described above:
+ // - verify the server
+ // - get the configuration from the server
+ var serverConfiguration = await ValidateServerAndRetrieveConfiguration(uri);
+ // use that server configuration to configure social providers such as twitter, google, etc.
+ ?? How to do that
+ // Also: how to update the configuration if it changes on the server?
return ValueTask.FromResult(_registrations.GetOrAdd(identifier, _ =>
{
var registration = new OpenIddictClientRegistration
{
RegistrationId = identifier,
Issuer = uri,
ProviderName = "User-defined authorization server",
ConfigurationEndpoint = new Uri(uri, ".well-known/openid-configuration"),
ClientId = "console",
PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative),
RedirectUri = new Uri("callback/login/local", UriKind.Relative),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }
};
// Note: unlike a static registration - for which the configuration manager is instantiated for you -
// a dynamic registration requires attaching it manually. Caching the instance is strongly recommended.
registration.ConfigurationManager = new ConfigurationManager<OpenIddictConfiguration>(
registration.ConfigurationEndpoint.AbsoluteUri, new OpenIddictClientRetriever(this, registration))
{
AutomaticRefreshInterval = ConfigurationManager<OpenIddictConfiguration>.DefaultAutomaticRefreshInterval,
RefreshInterval = ConfigurationManager<OpenIddictConfiguration>.DefaultRefreshInterval
};
return registration;
}));
}
}
What I do not like at all, is giving all the potentially sensitive configration data required for the |
@gentledepp looks like we're now a bit off-topic. If your company is sponsoring the project and you need assistance with your scenario, please open a dedicated thread and I'll add some thoughts there 😃 Cheers. |
I am a personal supporter. Please let me know if my/our compensation is not enough. Created a new issue: #2192 |
Support for professional projects - i.e non-hobby projects - is only offered to $25/month (and higher) sponsors, even if you're sponsoring OpenIddict using a personal GitHub account (otherwise, that would be a terribly easy way to abuse the system). I encourage you to ask your employer to sponsor the project as there's no reason it should come out of your own pocket 😄 |
still trying to get this approved. I am fiddeling on my own in the meantime 😅 |
No worries 👍🏻
AFAICT, GitHub has already taken your personal sponsorship cancellation into account. |
Is your feature request related to a problem?
I am trying to implement an IFTTT service and am using OpenIddict to handle authentication. Everything is working so far but they have an automated test suite for services which requires me to provide them with a valid auth token for a test user. Passing these automated tests is required for publishing services so this blocks me from publishing an IFTTT service that uses OpenIddict.
Describe the solution you'd like
I would like to be able to generate a token on behalf of a user/subject so that I can provide them to IFTTT's test system.
Additional context
IFTTT enforces that services restrict access to only IFTTT by validating the
IFTTT-Service-Key
authorization header so there should be no security concern here.The text was updated successfully, but these errors were encountered: