Osy#betaRun it on Osyrin: osyrin.com

Reference / Security

ProviderIcon — the mark on every "Continue with …" and "Link …" button

ProviderIcon(string provider) → Icons · AccountsSetup.Marks = new ProviderMarks() · override Icons For(string provider)

The UserAccounts kit draws each sign-in provider's mark on its buttons — "Continue with …" on the sign-in page, "Link …" on the account page — from one mapping, `ProviderIcon`. The GitHub, Google and Microsoft marks are built in; any other provider shows the plain sign-in glyph until the app gives it a mark of its own by deriving `ProviderMarks`.

preview2 examples compiled by CIsecuritykitauthenticationicons

Summary#

Every provider the app offers for signing in ([[security-accounts-kit#providers]]) gets a button on the sign-in page — Continue with GitHub — and one on the account page — Link GitHub. Both draw the provider's mark from the same mapping, so the two pages can never show different logos for one provider:

providermarkwhere it comes from
GitHubIcons.Githubbuilt in
GoogleIcons.Googlebuilt in
MicrosoftIcons.Microsoftbuilt in
any otherIcons.Login — the plain sign-in glyphuntil the app gives it one

The three built-in names are matched without regard to case, and the marks are drawn in the current text colour like every icon, so they follow the app's theme in light and dark. No app draws them; Icons.Github, Icons.Google and Icons.Microsoft are also there for any page of the app's own.

app Desk {
  use Osysharp.Ui;
  use Osysharp.Identity;
  use Osysharp.Mail;
  use Osysharp.UserAccounts;
  use Osysharp.EmailTemplates;
  use Osysharp.Oauth;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.UserAccounts;

[Role] enum AppRole { Authenticator, Agent }

app.Secrets = [ new Secret("AcmeClientSecret") ];
app.OAuthClients = [
  new OAuthClient("Acme") {
    Provider = OAuthProvider.Oidc, Capabilities = [OAuthCapability.Login],
    Issuer = "https://login.acme.example", ClientId = "desk", ClientSecret = Secret.AcmeClientSecret,
    Scopes = ["openid", "email", "profile"],
  },
];

app.Accounts = new AccountsSetup { AppName = "Acme Desk", SignupRole = AppRole.Agent, Marks = new DeskMarks() };

// The company's own sign-in shows a building; every other provider keeps the kit's mark.
public class DeskMarks : ProviderMarks {
  public override Icons For(string provider) { return provider == "Acme" ? Icons.Building : base.For(provider); }
}

[Route("/")] component Home() { render { Text("The desk"); } }

Signature#

public Icons ProviderIcon(string provider);                       // the one mapping — AccountsSetup.Marks.For(provider)
public class ProviderMarks { public virtual Icons For(string provider); }   // derive and override to add a mark
public class ProviderMark { string Provider; Icons Mark; }        // a provider and its mark, as a page is handed it
public Icons MarkOf(List<ProviderMark> marks, string provider);   // the mark a page draws, from what it was handed
// AccountsSetup.Marks = new ProviderMarks();   AccountsFacts.Marks · ProviderFacts.Marks

Description#

How does an app give another provider its mark?#

Derive ProviderMarks, override For, and set it as AccountsSetup.Marks — as the example does. Answer the providers you know and hand every other one back to base.For(provider), so GitHub, Google and Microsoft keep their built-in marks and an unknown one still gets the sign-in glyph rather than a blank square. provider is the name the provider is declared under in app.OAuthClients (OAuth clients (app.OAuthClients)) — the same name the button reads.

The mark can be any icon the app has, its own included: put acme.svg in model/icons/ and answer Icons.Acme. The kit's buttons take the app's icon vocabulary, merged with the built-ins ([[ui-icons#as-a-value]]), so there is nothing to register and the name is checked when the app compiles.

Where is the mapping read?#

On the server, like every other part of AccountsSetup — the setting never reaches a browser. The facts each page already fetches carry the answer: the sign-in page's (AccountsPageFacts().Marks) and the account page's (MyProviders().Marks) hold every offered provider with its mark, and the buttons draw from those with MarkOf. So a page of the app's own that takes a kit page over (routes and pages) draws the same marks by reading the same facts:

Button($"Continue with {p}", onPress: () => ContinueWith(p)) { Icon(MarkOf(facts.Marks, p), size: 18); }

ProviderIcon(provider) answers one provider directly, in a function or a test.

How do I test it?#

[Test]
void each_provider_shows_its_mark() {
  Assert.Equal(Icons.Building, ProviderIcon("Acme"));
  Assert.Equal(Icons.Github, ProviderIcon("GitHub"));
  Assert.Equal(Icons.Login, ProviderIcon("Okta"));
}

[Test]
void the_sign_in_page_is_handed_each_offered_providers_mark() {
  OfferedSignInProviders.Stub(() => new List<string> { "Acme", "Google" });
  var facts = AccountsPageFacts();
  Assert.Equal(Icons.Building, MarkOf(facts.Marks, "Acme"));
  Assert.Equal(Icons.Google, MarkOf(facts.Marks, "Google"));
}

Stub OfferedSignInProviders to say which providers are offered, rather than depending on whose client secrets the run has. A UI test sees the button by its words — Assert.Visible("Continue with Acme"); the mark itself is seen in a photograph (osy test --pixels and Ui.Shot).

See also#

Related

The UserAccounts kit — sign-in done properly, once

`Osysharp.UserAccounts` is the default way into an app: sign-in with a password or a provider (Google, GitHub, …)…

icons

Drop `.svg` files into `model/icons/` and render them with `Icon(Icons.Search)`. The name is checked at compile time…

OAuth clients (app.OAuthClients)

`app.OAuthClients` declares the third-party OAuth providers your app uses — for signing users in (Login), for…

OAuth account completion — Security.VerifyPendingOAuthEmail / LinkOAuthFromPending

The two server-side calls that finish an OAuth sign-in on your own pages. When someone signs in with a provider…