osaka-auth

Improve login UI of the authentication system of Osaka University to allow password autocompletion and redesigned on the theme of Sci-Fi to fit modern style.

// ==UserScript==
// @name            osaka-auth
// @name:en         Osaka University Login Redesigned
// @name:ja         阪大ログイン画面再設計版
// @name:zh         大阪大学登录界面重新设计版
// @namespace       https://yocjyet.dev/
// @version         3.0.0
// @author          Yo Cjyet
// @description:en  Improve login UI of the authentication system of Osaka University to allow password autocompletion and redesigned on the theme of Sci-Fi to fit modern style.
// @description:ja  阪大のログイン画面をSF風に再設計し、パスワード管理ソフトの自動記入不能の問題を修正した。
// @description:zh  改善大阪大学认证系统的登录界面,允许密码自动完成,并在科幻主题上重新设计,以适应现代风格。
// @license         MIT
// @icon            https://www.osaka-u.ac.jp/favicon.ico
// @match           https://ou-idp.auth.osaka-u.ac.jp/idp/*
// @description Improve login UI of the authentication system of Osaka University to allow password autocompletion and redesigned on the theme of Sci-Fi to fit modern style.
// ==/UserScript==

(function () {
  'use strict';

  (function() {
    const HTML = `<main>
  <h1>
    <svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 116.131 114.063">
      <path
        fill="currentColor"
        d="M115.627 48.364C112.563 25.037 95.65 6.081 73.418 0L58.065 15.351c19.595 0 35.952 13.871 39.79 32.33C81.611 49.536 67.4 58.079 58.064 70.514c-9.336-12.43-23.547-20.978-39.791-22.834 3.84-18.459 20.195-32.33 39.792-32.33L42.713 0C20.479 6.081 3.572 25.037.501 48.364A58.564 58.564 0 0 0 0 55.997c0 3.463.318 6.849.901 10.145A40.585 40.585 0 0 1 18.5 65.3c13.604 2.325 24.9 11.405 30.311 23.673a40.08 40.08 0 0 1 2.366 7.076c.698 2.99 1.081 6.1 1.081 9.302 0 2.849-.295 5.631-.853 8.319-.032.134-.065.262-.098.393h13.511c-.025-.131-.065-.259-.094-.393a40.822 40.822 0 0 1-.854-8.319c0-3.202.385-6.31 1.082-9.302a40.355 40.355 0 0 1 2.366-7.076c5.409-12.268 16.706-21.35 30.308-23.673a40.632 40.632 0 0 1 17.604.842c.58-3.296.9-6.682.9-10.145a58.043 58.043 0 0 0-.503-7.633z"
      />
    </svg>
    大阪大学
  </h1>

  <form name="login-form" method="post" action="/idp/authnPwd">
    <div class="input" title="大阪大学個人ID">
      <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"
        ><path
          d="M231.937 211.986a120.486 120.486 0 0 0-67.12-54.142a72 72 0 1 0-73.633 0a120.488 120.488 0 0 0-67.12 54.14a8 8 0 1 0 13.85 8.013a104.037 104.037 0 0 1 180.174.002a8 8 0 1 0 13.849-8.013zM72 96a56 56 0 1 1 56 56a56.064 56.064 0 0 1-56-56z"
          fill="currentColor"
        /></svg
      >
      <input type="text" id="username" name="USER_ID" autocomplete="username" />
    </div>
    <div class="input" title="パスワード">
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"
        ><path
          d="M127.994 112a27.998 27.998 0 0 0-8 54.83V184a8 8 0 1 0 16 0v-17.17a27.998 27.998 0 0 0-8-54.83zm0 40a12 12 0 1 1 12-12a12.014 12.014 0 0 1-12 12zm80-72h-108V52a28 28 0 1 1 56 0a8 8 0 0 0 16 0a44 44 0 1 0-88 0v28h-36a16.018 16.018 0 0 0-16 16v112a16.018 16.018 0 0 0 16 16h160a16.018 16.018 0 0 0 16-16V96a16.018 16.018 0 0 0-16-16zm0 128h-160V96h160l.01 112z"
          fill="currentColor"
        /></svg
      >
      <input type="password" id="password" name="USER_PASSWORD" autocomplete="current-password" />
    </div>
    <button id="login" title="ログイン" type="submit">LOGIN</button>
    <input type="hidden" name="IDButton" value="ログイン" />
    <input type="hidden" name="CHECK_BOX" value="" />
  </form>
  <form name="otp-form" method="post" action="/idp/otpAuth">
    <div class="input" title="MFA認証コード">
      <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"
        ><path
          fill="currentColor"
          d="m11 23.18l-2-2.001l-1.411 1.41L11 26l6-6l-1.41-1.41L11 23.18zM28 30h-4v-2h4V16h-4V8a4.005 4.005 0 0 0-4-4V2a6.007 6.007 0 0 1 6 6v6h2a2.002 2.002 0 0 1 2 2v12a2.002 2.002 0 0 1-2 2z"
        /><path
          fill="currentColor"
          d="M20 14h-2V8A6 6 0 0 0 6 8v6H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V16a2 2 0 0 0-2-2ZM8 8a4 4 0 0 1 8 0v6H8Zm12 20H4V16h16Z"
        /></svg
      >
      <input
        type="text"
        id="authcode"
        name="OTP_CODE"
        autocomplete="off"
        maxlength="7"
        pattern="[0-9][0-9][0-9] ?[0-9][0-9][0-9]"
        required
        placeholder="000 000"
      />
    </div>
    <div>
      <input type="checkbox" id="remember" name="STORE_OTP_AUTH_RESULT" checked />
      <label for="remember" title="このデバイスで30日間認証不要">
        <span class="custom-checkbox">
          <svg class="unchecked" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"
            ><path
              fill="currentColor"
              d="M7.483 5.26A2.486 2.486 0 0 0 5.29 7.422a39.402 39.402 0 0 0 0 9.154a2.486 2.486 0 0 0 2.193 2.163c2.977.333 6.057.333 9.034 0a2.486 2.486 0 0 0 2.192-2.163c.256-2.185.328-4.386.216-6.58a.2.2 0 0 1 .059-.152l1.038-1.04a.198.198 0 0 1 .339.125a40.903 40.903 0 0 1-.162 7.822c-.215 1.836-1.69 3.275-3.516 3.48a42.5 42.5 0 0 1-9.366 0c-1.827-.205-3.302-1.644-3.516-3.48a40.903 40.903 0 0 1 0-9.504c.214-1.837 1.69-3.275 3.516-3.48a42.502 42.502 0 0 1 9.366 0a3.989 3.989 0 0 1 1.76.64a.19.19 0 0 1 .025.295l-.803.803a.211.211 0 0 1-.25.033a2.488 2.488 0 0 0-.898-.28a41.001 41.001 0 0 0-9.034 0Z"
            /><path
              fill="currentColor"
              d="M21.03 6.03a.75.75 0 1 0-1.06-1.06l-8.47 8.47l-2.47-2.47a.75.75 0 1 0-1.06 1.06l3 3a.75.75 0 0 0 1.06 0l9-9Z"
            /></svg
          >

          <svg class="checked" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"
            ><path
              fill="currentColor"
              d="M7.317 3.769a42.502 42.502 0 0 1 9.366 0a4 4 0 0 1 2.82 1.667L11.5 13.439l-2.47-2.47a.75.75 0 1 0-1.06 1.061l3 3a.75.75 0 0 0 1.06 0l8.116-8.115c.022.11.04.22.053.333c.37 3.157.37 6.346 0 9.504c-.215 1.836-1.69 3.275-3.516 3.48a42.5 42.5 0 0 1-9.366 0c-1.827-.205-3.302-1.644-3.516-3.48a40.903 40.903 0 0 1 0-9.504c.214-1.837 1.69-3.275 3.516-3.48Z"
            /></svg
          >
        </span>
        Remember for 30 days
      </label>
    </div>
    <button id="auth" title="認証" type="submit">AUTHENTICATE</button>
    <button class="outline" id="kaijo" title="MFA一時解除">DISABLE MFA</button>
  </form>
  <form name="error" onsubmit="return false;">
    <h2 id="error-title">認証エラー</h2>
    <p id="error-text">
      ログインできません。「大阪大学個人ID」または「パスワード」が誤っている可能性があります。<br /><br
      />*「大阪大学個人ID」 のアルファベットは必ず小文字を入力してください (大文字を使用するとエラーになります)。
    </p>
    <button title="戻る" id="error-return">RETURN</button>
  </form>

  <div class="actions">
    <a href="https://web.auth.osaka-u.ac.jp/portal/" target="_blank" title="ポータルサイト">
      <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"
        ><path
          d="M151.965 215.992v-64h-48v64L39.97 216V115.539a8 8 0 0 1 2.618-5.92l79.994-72.734a8 8 0 0 1 10.764 0l80.005 72.734a8 8 0 0 1 2.619 5.92V216z"
          opacity=".2"
          fill="currentColor"
        /><path
          d="M239.97 208h-16v-92.461a16.037 16.037 0 0 0-5.236-11.839l-80.006-72.735a15.936 15.936 0 0 0-21.527 0L37.207 103.7a16.034 16.034 0 0 0-5.236 11.839V208h-16a8 8 0 1 0 0 16h224a8 8 0 0 0 0-16zm-192-92.462l79.995-72.734l80.006 72.735V208h-48.006v-48.008a16.018 16.018 0 0 0-16-16h-32a16.018 16.018 0 0 0-16 16V208H47.97zM143.966 208h-32v-48.008h32z"
          fill="currentColor"
        /></svg
      ></a
    >
    <a
      href="https://web.auth.osaka-u.ac.jp/portal/ja/security_contact.html"
      target="_blank"
      title="情報セキュリティに関する連絡先"
    >
      <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"
        ><path
          d="M32 128v56a16 16 0 0 0 16 16h16a16 16 0 0 0 16-16v-40a16 16 0 0 0-16-16z"
          opacity=".2"
          fill="currentColor"
        /><path
          d="M225.456 128h-32a16 16 0 0 0-16 16v40a16 16 0 0 0 16 16h16a16 16 0 0 0 16-16z"
          opacity=".2"
          fill="currentColor"
        /><path
          d="M202.72 54.188A103.282 103.282 0 0 0 129.464 24l-.735.002l-.736-.002A104 104 0 0 0 24 128v56a24.027 24.027 0 0 0 24 24h16a24.027 24.027 0 0 0 24-24v-40a24.027 24.027 0 0 0-24-24H40.355a88.02 88.02 0 0 1 88.313-79.998h.12A88.017 88.017 0 0 1 217.1 120h-23.645a24.027 24.027 0 0 0-24 24v40a24.027 24.027 0 0 0 24 24h16a23.894 23.894 0 0 0 8-1.376V208a24.027 24.027 0 0 1-24 24H136a8 8 0 0 0 0 16h57.456a40.046 40.046 0 0 0 40-40v-80a103.283 103.283 0 0 0-30.735-73.813zM64 136a8.01 8.01 0 0 1 8 8v40a8.01 8.01 0 0 1-8 8H48a8.01 8.01 0 0 1-8-8v-48zm145.456 56h-16a8.01 8.01 0 0 1-8-8v-40a8.01 8.01 0 0 1 8-8h24v48a8.01 8.01 0 0 1-8 8z"
          fill="currentColor"
        /></svg
      >
    </a>
    <a
      href="https://web.auth.osaka-u.ac.jp/portal/ja/okomariguide.html"
      target="_blank"
      title="ログインにお困りの場合はこちら"
    >
      <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"
        ><circle cx="128.002" cy="128" r="96" opacity=".2" fill="currentColor" /><path
          d="M128.002 24a104 104 0 1 0 104 104a104.118 104.118 0 0 0-104-104zm0 192a88 88 0 1 1 88-88a88.1 88.1 0 0 1-88 88zm12-36a12 12 0 1 1-12-12a12 12 0 0 1 12 12zm24-71.995a36.065 36.065 0 0 1-28 35.104v.896a8 8 0 0 1-16 0v-8a8 8 0 0 1 8-8a20 20 0 1 0-20-20a8 8 0 0 1-16 0a36 36 0 1 1 72 0z"
          fill="currentColor"
        /></svg
      >
    </a>
  </div>
</main>
`;
    const CSS = `@import url('https://fonts.googleapis.com/css2?family=Kiwi+Maru:wght@300;400;500&display=swap');

:root {
  --color-background: #16163e;
  --color-primary: #2b2b7b;
  --color-primary-light: #382c83;
  --color-primary-lighter: #257c8e;
  --color-secondary: #1ba493;
  --color-highlight: hwb(164 7% 20%);
  --color-accent: #07f49e;
  --color-desaturated: #506d6a;

  font-family: 'Kiwi Maru', sans-serif;
  font-size: 18px;
  color-scheme: light dark;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;

  --drop-shadow: drop-shadow(0 0 3px rgba(17, 204, 153, 0.5));
  --brightness: brightness(1.12);
  --box-shadow-focus: 0 0 1em rgba(0, 0, 0, 0.25), 0 0 3em rgba(17, 204, 153, 0.1);
}

* {
  box-sizing: border-box;
}

html {
  background: linear-gradient(-45deg, var(--color-background) 0%, var(--color-primary) 100%);
}

body {
  color: var(--color-highlight);
  margin: 0;
  display: grid;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
}

body.error {
  filter: hue-rotate(195deg) saturate(1.5) brightness(0.8) contrast(2);
  background: linear-gradient(-45deg, rgba(62, 5, 10, 1) 0%, rgba(113, 5, 29, 1) 100%);
}

body.error button {
  background: var(--color-secondary);
  color: black;
  filter: drop-shadow(0 0 3px rgba(17, 204, 153, 0.5));
}

body.error main {
  border: 2px solid var(--color-secondary);
}

main {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1em;
  margin: 0 auto;
  max-width: 25rem;
  backdrop-filter: blur(10px);
  background: rgba(0, 0, 0, 0.18);
  padding: 1.8em 3.1em;
  border-radius: 0.5em;
  box-shadow: 0 0 1em rgba(0, 0, 0, 0.25), 0 0 5em rgba(17, 204, 153, 0.1), inset 0 0 3em rgba(255, 255, 255, 0.05);
  border: 2px solid rgba(255, 255, 255, 0.1);
  text-shadow: 0 0 0.5em rgba(17, 204, 153, 0.2);
}

svg,
button,
input {
  filter: var(--drop-shadow);
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  margin: 0.2em 0;
}
.logo {
  width: 5rem;
}

h2#error-title {
  font-size: 1.2em;
  font-weight: 400;
  margin: 0;
}

p#error-text {
  font-size: 0.8em;
  font-weight: 300;
  margin: 0;
  margin-bottom: 1em;
  text-align: center;
}

button,
input {
  color: inherit;
  font-size: inherit;
  font-family: inherit;
}

button {
  width: 100%;
  background: linear-gradient(90deg, var(--color-secondary) 0%, var(--color-accent) 100%);
  color: white;
  border: none;
  border-radius: 0.25em;
  padding: 0.8em 0;
  font-weight: bold;
  transition: all 0.1s ease-in-out;
  cursor: pointer;
}
button:focus {
  outline: none;
  filter: var(--drop-shadow) var(--brightness);
  transform: scale(1.01);
  box-shadow: var(--box-shadow-focus);
}
button:hover {
  filter: var(--drop-shadow) var(--brightness);
  transform: scale(1.01);
}

button.outline {
  background: transparent;
  border: 1px solid var(--color-secondary);
  color: var(--color-secondary);
  transition: all 0.1s ease-in-out;
}

.input {
  display: flex;
  align-items: center;
  gap: 0.5em;
  border: 1px solid var(--color-secondary);
  padding: 0.85em 0.5em;
  border-radius: 0.25em;
  color: var(--color-secondary);
  transition: all 0.1s ease-in-out;
  width: 100%;
}
.input > svg {
  width: 1.5em;
  height: 1.5em;
}
.input > input {
  border: none;
  background: transparent;
  width: 100%;
}

.input:focus-within {
  border: 1px solid var(--color-accent);
  color: var(--color-accent);
}
.input > input:focus {
  outline: none;
}
.input:hover {
  border: 1px solid var(--color-highlight);
  color: var(--color-highlight);
}

input::placeholder {
  color: var(--color-desaturated);
}

.actions {
  color: var(--color-primary-lighter);
  display: flex;
  align-items: center;
  gap: 1.2em;
  padding: 0.1em;
}
.actions > a,
.actions > a:visited {
  color: inherit;
  transition: all 0.1s ease-in-out;
}
.actions > a:hover {
  color: var(--color-highlight);
}
.actions > a:focus {
  outline: none;
  color: var(--color-accent);
  transform: scale(1.1);
}

input[type='checkbox'] {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}
input[type='checkbox'] + label {
  display: flex;
  align-items: center;
  gap: 0.5em;
  cursor: pointer;
  --checked: none;
  --unchecked: block;
}

input[type='checkbox']:checked + label {
  --checked: block;
  --unchecked: none;
}

input[type='checkbox'] + label > .custom-checkbox > .checked {
  display: var(--checked);
}
input[type='checkbox'] + label > .custom-checkbox > .unchecked {
  display: var(--unchecked);
}

.custom-checkbox {
  display: flex;
  align-items: center;
  gap: 0.5em;
  cursor: pointer;
}

form {
  display: none;
}

td[bgcolor='#FFFFFF'] {
  display: none;
}

table.style_table2 td.style_td {
  padding: 0.25em;
}

table.style_table2 th[colspan='5'] {
  padding: 0.5em 0;
  font-size: large;
  font-weight: bold;
}

div[align='center'] > br {
  display: none;
}

div[align='center'] {
  padding: 1em 0;
}

button#ok {
  width: 100%;
  background: linear-gradient(90deg, var(--color-secondary) 0%, var(--color-accent) 100%);
  color: white;
  border: none;
  border-radius: 0.25em;
  padding: 0.8em 0;
  font-weight: bold;
  transition: all 0.1s ease-in-out;
  cursor: pointer;
}
`;
    const STYLES = document.querySelectorAll('link[rel="stylesheet"]');
    for (const style of [...STYLES]) {
      style.remove();
    }
    function addStyle(css) {
      const style = document.createElement("style");
      style.innerHTML = css;
      document.head.appendChild(style);
    }
    addStyle(`
      body > *:not(main) {
        display: none;
      }
  `);
    const ICON_URL = "https://www.osaka-u.ac.jp/favicon.ico";
    const ICON_LINK = document.createElement("link");
    ICON_LINK.setAttribute("rel", "icon");
    ICON_LINK.setAttribute("type", "image/png");
    ICON_LINK.setAttribute("href", ICON_URL);
    document.head.appendChild(ICON_LINK);
    const main = document.createElement("main");
    document.body.appendChild(main);
    main.outerHTML = HTML;
    addStyle(CSS);
    function displayForm(formName) {
      for (let i = 0; i < document.forms.length; i++) {
        const form = document.forms[i];
        form.style.display = form.name === formName ? "contents" : "none";
      }
    }
    const h2 = document.querySelector("table h2");
    const FORMS = {
      ログイン: "login-form",
      MFA認証コード入力: "otp-form",
      Login: "login-form",
      "Enter MFA code": "otp-form"
    };
    const errorH1 = document.querySelector("h1.errorh1");
    if (errorH1 !== null && errorH1.textContent) {
      const errorTitle = errorH1.textContent.trim();
      const errorTable = errorH1.nextElementSibling;
      if (errorTable === null) {
        console.error("Error table not found");
        return;
      }
      const errorRows = errorTable.querySelectorAll("tr");
      const errorText = errorRows[0].innerHTML.trim();
      document.querySelector("h2#error-title").textContent = errorTitle;
      document.querySelector("p#error-text").innerHTML = errorText;
      const returnButton = document.querySelector("button#error-return");
      const errorReturn = errorTable.querySelector("a");
      if (errorReturn) {
        const returnHref = errorReturn.href.trim();
        const match = /sentHref\('(.*)'\)/.exec(returnHref);
        if (match && match.length > 1) {
          const link = match[1];
          returnButton.addEventListener("click", () => {
            window.sentHref(link);
          });
        } else {
          console.error("Error return link href is invalid");
          returnButton.style.display = "none";
          return;
        }
      } else {
        console.error("Return link not found");
        returnButton.style.display = "none";
      }
      document.body.classList.add("error");
      displayForm("error");
      return;
    }
    if (h2 === null) {
      const form = document.querySelector('form[name="cmdForm"]');
      if (form !== null) {
        const main2 = document.querySelector("main");
        main2.appendChild(form);
        form.style.display = "block";
        return;
      }
      console.error("h2 not found");
      return;
    }
    function isValidFormTitle(title) {
      return title in FORMS;
    }
    const formTitle = h2.textContent.trim();
    if (!isValidFormTitle(formTitle)) {
      console.error("unknown form: " + h2.textContent.trim());
      return;
    }
    displayForm(FORMS[formTitle]);
    const otpForm = document.querySelector('form[name="otp-form"]');
    otpForm == null ? void 0 : otpForm.addEventListener("submit", () => {
      const authcode = document.getElementById("authcode");
      authcode.value = authcode.value.replace(/ /g, "");
      const remember = document.getElementById("remember");
      if (remember.checked) {
        remember.value = "1";
      } else {
        remember.value = "0";
      }
      return true;
    });
  })();

})();